I'm working through linter complaints, these are some fixes. Broad categories: 1) Ignore errors where we can ignore errors: add "_ = ..." construct. you can argue that this is annoying noise, but apart from silencing the linter it *does* serve the purpose of highlighting that an error is being ignored. I think this is OK, because the linter highlighted some error cases I wasn't aware of (starting CPU profiles, for example). 2) Untyped constants where we though we had set the type. 3) A real bug where we ineffectually assigned to a shadowed err. 4) Some dead code removed. There'll be more of these, because not all packages are fixed, but the diff was already large enough.
455 lines
13 KiB
Go
455 lines
13 KiB
Go
// Copyright (C) 2014 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 (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"net"
|
|
"net/http"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"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"
|
|
)
|
|
|
|
// 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 = 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, connectionsService connectionsIntf, version int, preview bool) map[string]interface{} {
|
|
opts := cfg.Options()
|
|
res := make(map[string]interface{})
|
|
res["urVersion"] = version
|
|
res["uniqueID"] = opts.URUniqueID
|
|
res["version"] = Version
|
|
res["longVersion"] = LongVersion
|
|
res["platform"] = runtime.GOOS + "-" + runtime.GOARCH
|
|
res["numFolders"] = len(cfg.Folders())
|
|
res["numDevices"] = len(cfg.Devices())
|
|
|
|
var totFiles, maxFiles int
|
|
var totBytes, maxBytes int64
|
|
for folderID := range cfg.Folders() {
|
|
global := m.GlobalSize(folderID)
|
|
totFiles += int(global.Files)
|
|
totBytes += global.Bytes
|
|
if int(global.Files) > maxFiles {
|
|
maxFiles = int(global.Files)
|
|
}
|
|
if global.Bytes > maxBytes {
|
|
maxBytes = global.Bytes
|
|
}
|
|
}
|
|
|
|
res["totFiles"] = totFiles
|
|
res["folderMaxFiles"] = maxFiles
|
|
res["totMiB"] = totBytes / 1024 / 1024
|
|
res["folderMaxMiB"] = maxBytes / 1024 / 1024
|
|
|
|
var mem runtime.MemStats
|
|
runtime.ReadMemStats(&mem)
|
|
res["memoryUsageMiB"] = (mem.Sys - mem.HeapReleased) / 1024 / 1024
|
|
res["sha256Perf"] = cpuBench(5, 125*time.Millisecond, false)
|
|
res["hashPerf"] = cpuBench(5, 125*time.Millisecond, true)
|
|
|
|
bytes, err := memorySize()
|
|
if err == nil {
|
|
res["memorySize"] = bytes / 1024 / 1024
|
|
}
|
|
res["numCPU"] = runtime.NumCPU()
|
|
|
|
var rescanIntvs []int
|
|
folderUses := map[string]int{
|
|
"sendonly": 0,
|
|
"sendreceive": 0,
|
|
"receiveonly": 0,
|
|
"ignorePerms": 0,
|
|
"ignoreDelete": 0,
|
|
"autoNormalize": 0,
|
|
"simpleVersioning": 0,
|
|
"externalVersioning": 0,
|
|
"staggeredVersioning": 0,
|
|
"trashcanVersioning": 0,
|
|
}
|
|
for _, cfg := range cfg.Folders() {
|
|
rescanIntvs = append(rescanIntvs, cfg.RescanIntervalS)
|
|
|
|
switch cfg.Type {
|
|
case config.FolderTypeSendOnly:
|
|
folderUses["sendonly"]++
|
|
case config.FolderTypeSendReceive:
|
|
folderUses["sendreceive"]++
|
|
case config.FolderTypeReceiveOnly:
|
|
folderUses["receiveonly"]++
|
|
}
|
|
if cfg.IgnorePerms {
|
|
folderUses["ignorePerms"]++
|
|
}
|
|
if cfg.IgnoreDelete {
|
|
folderUses["ignoreDelete"]++
|
|
}
|
|
if cfg.AutoNormalize {
|
|
folderUses["autoNormalize"]++
|
|
}
|
|
if cfg.Versioning.Type != "" {
|
|
folderUses[cfg.Versioning.Type+"Versioning"]++
|
|
}
|
|
}
|
|
sort.Ints(rescanIntvs)
|
|
res["rescanIntvs"] = rescanIntvs
|
|
res["folderUses"] = folderUses
|
|
|
|
deviceUses := map[string]int{
|
|
"introducer": 0,
|
|
"customCertName": 0,
|
|
"compressAlways": 0,
|
|
"compressMetadata": 0,
|
|
"compressNever": 0,
|
|
"dynamicAddr": 0,
|
|
"staticAddr": 0,
|
|
}
|
|
for _, cfg := range cfg.Devices() {
|
|
if cfg.Introducer {
|
|
deviceUses["introducer"]++
|
|
}
|
|
if cfg.CertName != "" && cfg.CertName != "syncthing" {
|
|
deviceUses["customCertName"]++
|
|
}
|
|
if cfg.Compression == protocol.CompressAlways {
|
|
deviceUses["compressAlways"]++
|
|
} else if cfg.Compression == protocol.CompressMetadata {
|
|
deviceUses["compressMetadata"]++
|
|
} else if cfg.Compression == protocol.CompressNever {
|
|
deviceUses["compressNever"]++
|
|
}
|
|
for _, addr := range cfg.Addresses {
|
|
if addr == "dynamic" {
|
|
deviceUses["dynamicAddr"]++
|
|
} else {
|
|
deviceUses["staticAddr"]++
|
|
}
|
|
}
|
|
}
|
|
res["deviceUses"] = deviceUses
|
|
|
|
defaultAnnounceServersDNS, defaultAnnounceServersIP, otherAnnounceServers := 0, 0, 0
|
|
for _, addr := range opts.GlobalAnnServers {
|
|
if addr == "default" || addr == "default-v4" || addr == "default-v6" {
|
|
defaultAnnounceServersDNS++
|
|
} else {
|
|
otherAnnounceServers++
|
|
}
|
|
}
|
|
res["announce"] = map[string]interface{}{
|
|
"globalEnabled": opts.GlobalAnnEnabled,
|
|
"localEnabled": opts.LocalAnnEnabled,
|
|
"defaultServersDNS": defaultAnnounceServersDNS,
|
|
"defaultServersIP": defaultAnnounceServersIP,
|
|
"otherServers": otherAnnounceServers,
|
|
}
|
|
|
|
defaultRelayServers, otherRelayServers := 0, 0
|
|
for _, addr := range cfg.ListenAddresses() {
|
|
switch {
|
|
case addr == "dynamic+https://relays.syncthing.net/endpoint":
|
|
defaultRelayServers++
|
|
case strings.HasPrefix(addr, "relay://") || strings.HasPrefix(addr, "dynamic+http"):
|
|
otherRelayServers++
|
|
}
|
|
}
|
|
res["relays"] = map[string]interface{}{
|
|
"enabled": defaultRelayServers+otherAnnounceServers > 0,
|
|
"defaultServers": defaultRelayServers,
|
|
"otherServers": otherRelayServers,
|
|
}
|
|
|
|
res["usesRateLimit"] = opts.MaxRecvKbps > 0 || opts.MaxSendKbps > 0
|
|
|
|
res["upgradeAllowedManual"] = !(upgrade.DisabledByCompilation || noUpgradeFromEnv)
|
|
res["upgradeAllowedAuto"] = !(upgrade.DisabledByCompilation || noUpgradeFromEnv) && opts.AutoUpgradeIntervalH > 0
|
|
res["upgradeAllowedPre"] = !(upgrade.DisabledByCompilation || noUpgradeFromEnv) && opts.AutoUpgradeIntervalH > 0 && opts.UpgradeToPreReleases
|
|
|
|
if version >= 3 {
|
|
res["uptime"] = int(time.Now().Sub(startTime).Seconds())
|
|
res["natType"] = connectionsService.NATType()
|
|
res["alwaysLocalNets"] = len(opts.AlwaysLocalNets) > 0
|
|
res["cacheIgnoredFiles"] = opts.CacheIgnoredFiles
|
|
res["overwriteRemoteDeviceNames"] = opts.OverwriteRemoteDevNames
|
|
res["progressEmitterEnabled"] = opts.ProgressUpdateIntervalS > -1
|
|
res["customDefaultFolderPath"] = opts.DefaultFolderPath != "~"
|
|
res["customTrafficClass"] = opts.TrafficClass != 0
|
|
res["customTempIndexMinBlocks"] = opts.TempIndexMinBlocks != 10
|
|
res["temporariesDisabled"] = opts.KeepTemporariesH == 0
|
|
res["temporariesCustom"] = opts.KeepTemporariesH != 24
|
|
res["limitBandwidthInLan"] = opts.LimitBandwidthInLan
|
|
res["customReleaseURL"] = opts.ReleasesURL != "https://upgrades.syncthing.net/meta.json"
|
|
res["restartOnWakeup"] = opts.RestartOnWakeup
|
|
|
|
folderUsesV3 := map[string]int{
|
|
"scanProgressDisabled": 0,
|
|
"conflictsDisabled": 0,
|
|
"conflictsUnlimited": 0,
|
|
"conflictsOther": 0,
|
|
"disableSparseFiles": 0,
|
|
"disableTempIndexes": 0,
|
|
"alwaysWeakHash": 0,
|
|
"customWeakHashThreshold": 0,
|
|
"fsWatcherEnabled": 0,
|
|
}
|
|
pullOrder := make(map[string]int)
|
|
filesystemType := make(map[string]int)
|
|
var fsWatcherDelays []int
|
|
for _, cfg := range cfg.Folders() {
|
|
if cfg.ScanProgressIntervalS < 0 {
|
|
folderUsesV3["scanProgressDisabled"]++
|
|
}
|
|
if cfg.MaxConflicts == 0 {
|
|
folderUsesV3["conflictsDisabled"]++
|
|
} else if cfg.MaxConflicts < 0 {
|
|
folderUsesV3["conflictsUnlimited"]++
|
|
} else {
|
|
folderUsesV3["conflictsOther"]++
|
|
}
|
|
if cfg.DisableSparseFiles {
|
|
folderUsesV3["disableSparseFiles"]++
|
|
}
|
|
if cfg.DisableTempIndexes {
|
|
folderUsesV3["disableTempIndexes"]++
|
|
}
|
|
if cfg.WeakHashThresholdPct < 0 {
|
|
folderUsesV3["alwaysWeakHash"]++
|
|
} else if cfg.WeakHashThresholdPct != 25 {
|
|
folderUsesV3["customWeakHashThreshold"]++
|
|
}
|
|
if cfg.FSWatcherEnabled {
|
|
folderUsesV3["fsWatcherEnabled"]++
|
|
}
|
|
pullOrder[cfg.Order.String()]++
|
|
filesystemType[cfg.FilesystemType.String()]++
|
|
fsWatcherDelays = append(fsWatcherDelays, cfg.FSWatcherDelayS)
|
|
}
|
|
sort.Ints(fsWatcherDelays)
|
|
folderUsesV3Interface := map[string]interface{}{
|
|
"pullOrder": pullOrder,
|
|
"filesystemType": filesystemType,
|
|
"fsWatcherDelays": fsWatcherDelays,
|
|
}
|
|
for key, value := range folderUsesV3 {
|
|
folderUsesV3Interface[key] = value
|
|
}
|
|
res["folderUsesV3"] = folderUsesV3Interface
|
|
|
|
guiCfg := cfg.GUI()
|
|
// Anticipate multiple GUI configs in the future, hence store counts.
|
|
guiStats := map[string]int{
|
|
"enabled": 0,
|
|
"useTLS": 0,
|
|
"useAuth": 0,
|
|
"insecureAdminAccess": 0,
|
|
"debugging": 0,
|
|
"insecureSkipHostCheck": 0,
|
|
"insecureAllowFrameLoading": 0,
|
|
"listenLocal": 0,
|
|
"listenUnspecified": 0,
|
|
}
|
|
theme := make(map[string]int)
|
|
if guiCfg.Enabled {
|
|
guiStats["enabled"]++
|
|
if guiCfg.UseTLS() {
|
|
guiStats["useTLS"]++
|
|
}
|
|
if len(guiCfg.User) > 0 && len(guiCfg.Password) > 0 {
|
|
guiStats["useAuth"]++
|
|
}
|
|
if guiCfg.InsecureAdminAccess {
|
|
guiStats["insecureAdminAccess"]++
|
|
}
|
|
if guiCfg.Debugging {
|
|
guiStats["debugging"]++
|
|
}
|
|
if guiCfg.InsecureSkipHostCheck {
|
|
guiStats["insecureSkipHostCheck"]++
|
|
}
|
|
if guiCfg.InsecureAllowFrameLoading {
|
|
guiStats["insecureAllowFrameLoading"]++
|
|
}
|
|
|
|
addr, err := net.ResolveTCPAddr("tcp", guiCfg.Address())
|
|
if err == nil {
|
|
if addr.IP.IsLoopback() {
|
|
guiStats["listenLocal"]++
|
|
} else if addr.IP.IsUnspecified() {
|
|
guiStats["listenUnspecified"]++
|
|
}
|
|
}
|
|
|
|
theme[guiCfg.Theme]++
|
|
}
|
|
guiStatsInterface := map[string]interface{}{
|
|
"theme": theme,
|
|
}
|
|
for key, value := range guiStats {
|
|
guiStatsInterface[key] = value
|
|
}
|
|
res["guiStats"] = guiStatsInterface
|
|
}
|
|
|
|
for key, value := range m.UsageReportingStats(version, preview) {
|
|
res[key] = value
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
type usageReportingService struct {
|
|
cfg *config.Wrapper
|
|
model *model.Model
|
|
connectionsService *connections.Service
|
|
forceRun chan struct{}
|
|
stop chan struct{}
|
|
stopped chan struct{}
|
|
stopMut sync.RWMutex
|
|
}
|
|
|
|
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{}),
|
|
stopped: make(chan struct{}),
|
|
}
|
|
close(svc.stopped) // Not yet running, dont block on Stop()
|
|
cfg.Subscribe(svc)
|
|
return svc
|
|
}
|
|
|
|
func (s *usageReportingService) sendUsageReport() error {
|
|
d := reportData(s.cfg, s.model, s.connectionsService, s.cfg.Options().URAccepted, false)
|
|
var b bytes.Buffer
|
|
if err := json.NewEncoder(&b).Encode(d); err != nil {
|
|
return err
|
|
}
|
|
|
|
client := &http.Client{
|
|
Transport: &http.Transport{
|
|
Dial: dialer.Dial,
|
|
Proxy: http.ProxyFromEnvironment,
|
|
TLSClientConfig: &tls.Config{
|
|
InsecureSkipVerify: s.cfg.Options().URPostInsecurely,
|
|
},
|
|
},
|
|
}
|
|
_, err := client.Post(s.cfg.Options().URURL, "application/json", &b)
|
|
return err
|
|
}
|
|
|
|
func (s *usageReportingService) Serve() {
|
|
s.stopMut.Lock()
|
|
s.stop = make(chan struct{})
|
|
s.stopped = make(chan struct{})
|
|
s.stopMut.Unlock()
|
|
t := time.NewTimer(time.Duration(s.cfg.Options().URInitialDelayS) * time.Second)
|
|
s.stopMut.RLock()
|
|
defer func() {
|
|
close(s.stopped)
|
|
s.stopMut.RUnlock()
|
|
}()
|
|
for {
|
|
select {
|
|
case <-s.stop:
|
|
return
|
|
case <-s.forceRun:
|
|
t.Reset(0)
|
|
case <-t.C:
|
|
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.stopMut.RLock()
|
|
select {
|
|
case s.forceRun <- struct{}{}:
|
|
case <-s.stop:
|
|
}
|
|
s.stopMut.RUnlock()
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (s *usageReportingService) Stop() {
|
|
s.stopMut.RLock()
|
|
close(s.stop)
|
|
<-s.stopped
|
|
s.stopMut.RUnlock()
|
|
}
|
|
|
|
func (*usageReportingService) String() string {
|
|
return "usageReportingService"
|
|
}
|
|
|
|
// cpuBench returns CPU performance as a measure of single threaded SHA-256 MiB/s
|
|
func cpuBench(iterations int, duration time.Duration, useWeakHash bool) float64 {
|
|
dataSize := 16 * protocol.MinBlockSize
|
|
bs := make([]byte, dataSize)
|
|
_, _ = rand.Reader.Read(bs)
|
|
|
|
var perf float64
|
|
for i := 0; i < iterations; i++ {
|
|
if v := cpuBenchOnce(duration, useWeakHash, bs); v > perf {
|
|
perf = v
|
|
}
|
|
}
|
|
blocksResult = nil
|
|
return perf
|
|
}
|
|
|
|
var blocksResult []protocol.BlockInfo // so the result is not optimized away
|
|
|
|
func cpuBenchOnce(duration time.Duration, useWeakHash bool, bs []byte) float64 {
|
|
t0 := time.Now()
|
|
b := 0
|
|
for time.Since(t0) < duration {
|
|
r := bytes.NewReader(bs)
|
|
blocksResult, _ = scanner.Blocks(context.TODO(), r, protocol.MinBlockSize, int64(len(bs)), nil, useWeakHash)
|
|
b += len(bs)
|
|
}
|
|
d := time.Since(t0)
|
|
return float64(int(float64(b)/d.Seconds()/(1<<20)*100)) / 100
|
|
}
|