lib: Apply config changes sequentially (ref #5298) (#7188)

This commit is contained in:
Simon Frei 2021-01-15 15:43:34 +01:00 committed by GitHub
parent b2d82da20d
commit f63cdbfcfa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1032 additions and 790 deletions

View File

@ -186,10 +186,10 @@ func main() {
}
wrapper := config.Wrap("config", config.New(id), id, events.NoopLogger)
wrapper.SetOptions(config.OptionsConfiguration{
NATLeaseM: natLease,
NATRenewalM: natRenewal,
NATTimeoutS: natTimeout,
wrapper.Modify(func(cfg *config.Configuration) {
cfg.Options.NATLeaseM = natLease
cfg.Options.NATRenewalM = natRenewal
cfg.Options.NATTimeoutS = natTimeout
})
natSvc := nat.NewService(id, wrapper)
mapping := mapping{natSvc.NewMapping(nat.TCP, addr.IP, addr.Port)}

View File

@ -607,7 +607,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
go evLogger.Serve(ctx)
defer cancel()
cfg, err := syncthing.LoadConfigAtStartup(locations.Get(locations.ConfigFile), cert, evLogger, runtimeOptions.allowNewerConfig, noDefaultFolder)
cfgWrapper, err := syncthing.LoadConfigAtStartup(locations.Get(locations.ConfigFile), cert, evLogger, runtimeOptions.allowNewerConfig, noDefaultFolder)
if err != nil {
l.Warnln("Failed to initialize config:", err)
os.Exit(svcutil.ExitError.AsInt())
@ -618,20 +618,20 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
// environment variable is set.
if build.IsCandidate && !upgrade.DisabledByCompilation && !runtimeOptions.NoUpgrade {
l.Infoln("Automatic upgrade is always enabled for candidate releases.")
if opts := cfg.Options(); opts.AutoUpgradeIntervalH == 0 || opts.AutoUpgradeIntervalH > 24 {
opts.AutoUpgradeIntervalH = 12
// Set the option into the config as well, as the auto upgrade
// loop expects to read a valid interval from there.
cfg.SetOptions(opts)
cfg.Save()
}
// We don't tweak the user's choice of upgrading to pre-releases or
// not, as otherwise they cannot step off the candidate channel.
cfgWrapper.Modify(func(cfg *config.Configuration) {
l.Infoln("Automatic upgrade is always enabled for candidate releases.")
if cfg.Options.AutoUpgradeIntervalH == 0 || cfg.Options.AutoUpgradeIntervalH > 24 {
cfg.Options.AutoUpgradeIntervalH = 12
// Set the option into the config as well, as the auto upgrade
// loop expects to read a valid interval from there.
}
// We don't tweak the user's choice of upgrading to pre-releases or
// not, as otherwise they cannot step off the candidate channel.
})
}
dbFile := locations.Get(locations.Database)
ldb, err := syncthing.OpenDBBackend(dbFile, cfg.Options().DatabaseTuning)
ldb, err := syncthing.OpenDBBackend(dbFile, cfgWrapper.Options().DatabaseTuning)
if err != nil {
l.Warnln("Error opening database:", err)
os.Exit(1)
@ -642,7 +642,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
// later after App is initialised.
autoUpgradePossible := autoUpgradePossible(runtimeOptions)
if autoUpgradePossible && cfg.Options().AutoUpgradeEnabled() {
if autoUpgradePossible && cfgWrapper.Options().AutoUpgradeEnabled() {
// try to do upgrade directly and log the error if relevant.
release, err := initialAutoUpgradeCheck(db.NewMiscDataNamespace(ldb))
if err == nil {
@ -661,9 +661,9 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
}
if runtimeOptions.unpaused {
setPauseState(cfg, false)
setPauseState(cfgWrapper, false)
} else if runtimeOptions.paused {
setPauseState(cfg, true)
setPauseState(cfgWrapper, true)
}
appOpts := runtimeOptions.Options
@ -681,14 +681,14 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
appOpts.DBIndirectGCInterval = dur
}
app, err := syncthing.New(cfg, ldb, evLogger, cert, appOpts)
app, err := syncthing.New(cfgWrapper, ldb, evLogger, cert, appOpts)
if err != nil {
l.Warnln("Failed to start Syncthing:", err)
os.Exit(svcutil.ExitError.AsInt())
}
if autoUpgradePossible {
go autoUpgrade(cfg, app, evLogger)
go autoUpgrade(cfgWrapper, app, evLogger)
}
setupSignalHandling(app)
@ -709,7 +709,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
}
}
go standbyMonitor(app, cfg)
go standbyMonitor(app, cfgWrapper)
if err := app.Start(); err != nil {
os.Exit(svcutil.ExitError.AsInt())
@ -717,10 +717,10 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
cleanConfigDirectory()
if cfg.Options().StartBrowser && !runtimeOptions.noBrowser && !runtimeOptions.stRestarting {
if cfgWrapper.Options().StartBrowser && !runtimeOptions.noBrowser && !runtimeOptions.stRestarting {
// Can potentially block if the utility we are invoking doesn't
// fork, and just execs, hence keep it in its own routine.
go func() { _ = openURL(cfg.GUI().URL()) }()
go func() { _ = openURL(cfgWrapper.GUI().URL()) }()
}
status := app.Wait()
@ -988,15 +988,16 @@ func showPaths(options RuntimeOptions) {
fmt.Printf("Default sync folder directory:\n\t%s\n\n", locations.Get(locations.DefFolder))
}
func setPauseState(cfg config.Wrapper, paused bool) {
raw := cfg.RawCopy()
for i := range raw.Devices {
raw.Devices[i].Paused = paused
}
for i := range raw.Folders {
raw.Folders[i].Paused = paused
}
if _, err := cfg.Replace(raw); err != nil {
func setPauseState(cfgWrapper config.Wrapper, paused bool) {
_, err := cfgWrapper.Modify(func(cfg *config.Configuration) {
for i := range cfg.Devices {
cfg.Devices[i].Paused = paused
}
for i := range cfg.Folders {
cfg.Folders[i].Paused = paused
}
})
if err != nil {
l.Warnln("Cannot adjust paused state:", err)
os.Exit(svcutil.ExitError.AsInt())
}

View File

@ -294,7 +294,6 @@ func (s *service) Serve(ctx context.Context) error {
Router: restMux,
id: s.id,
cfg: s.cfg,
mut: sync.NewMutex(),
}
configBuilder.registerConfig("/rest/config")
@ -1402,31 +1401,36 @@ func (s *service) makeDevicePauseHandler(paused bool) http.HandlerFunc {
var qs = r.URL.Query()
var deviceStr = qs.Get("device")
var cfgs []config.DeviceConfiguration
if deviceStr == "" {
for _, cfg := range s.cfg.Devices() {
cfg.Paused = paused
cfgs = append(cfgs, cfg)
var msg string
var status int
_, err := s.cfg.Modify(func(cfg *config.Configuration) {
if deviceStr == "" {
for i := range cfg.Devices {
cfg.Devices[i].Paused = paused
}
return
}
} else {
device, err := protocol.DeviceIDFromString(deviceStr)
if err != nil {
http.Error(w, err.Error(), 500)
msg = err.Error()
status = 500
return
}
cfg, ok := s.cfg.Devices()[device]
_, i, ok := cfg.Device(device)
if !ok {
http.Error(w, "not found", http.StatusNotFound)
msg = "not found"
status = http.StatusNotFound
return
}
cfg.Paused = paused
cfgs = append(cfgs, cfg)
}
cfg.Devices[i].Paused = paused
})
if _, err := s.cfg.SetDevices(cfgs); err != nil {
if msg != "" {
http.Error(w, msg, status)
} else if err != nil {
http.Error(w, err.Error(), 500)
}
}

View File

@ -1253,6 +1253,11 @@ func TestConfigChanges(t *testing.T) {
defer os.Remove(tmpFile.Name())
w := config.Wrap(tmpFile.Name(), cfg, protocol.LocalDeviceID, events.NoopLogger)
tmpFile.Close()
if cfgService, ok := w.(suture.Service); ok {
cfgCtx, cfgCancel := context.WithCancel(context.Background())
go cfgService.Serve(cfgCtx)
defer cfgCancel()
}
baseURL, cancel, err := startHTTP(w)
if err != nil {
t.Fatal("Unexpected error from getting base URL:", err)

View File

@ -17,14 +17,12 @@ import (
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/sync"
)
type configMuxBuilder struct {
*httprouter.Router
id protocol.DeviceID
cfg config.Wrapper
mut sync.Mutex
}
func (c *configMuxBuilder) registerConfig(path string) {
@ -59,14 +57,14 @@ func (c *configMuxBuilder) registerFolders(path string) {
})
c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
c.mut.Lock()
defer c.mut.Unlock()
var folders []config.FolderConfiguration
if err := unmarshalTo(r.Body, &folders); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
waiter, err := c.cfg.SetFolders(folders)
waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
cfg.SetFolders(folders)
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -75,19 +73,7 @@ func (c *configMuxBuilder) registerFolders(path string) {
})
c.HandlerFunc(http.MethodPost, path, func(w http.ResponseWriter, r *http.Request) {
c.mut.Lock()
defer c.mut.Unlock()
var folder config.FolderConfiguration
if err := unmarshalTo(r.Body, &folder); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
waiter, err := c.cfg.SetFolder(folder)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
c.finish(w, waiter)
c.adjustFolder(w, r, config.FolderConfiguration{})
})
}
@ -97,14 +83,14 @@ func (c *configMuxBuilder) registerDevices(path string) {
})
c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
c.mut.Lock()
defer c.mut.Unlock()
var devices []config.DeviceConfiguration
if err := unmarshalTo(r.Body, &devices); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
waiter, err := c.cfg.SetDevices(devices)
waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
cfg.SetDevices(devices)
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -113,14 +99,14 @@ func (c *configMuxBuilder) registerDevices(path string) {
})
c.HandlerFunc(http.MethodPost, path, func(w http.ResponseWriter, r *http.Request) {
c.mut.Lock()
defer c.mut.Unlock()
var device config.DeviceConfiguration
if err := unmarshalTo(r.Body, &device); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
waiter, err := c.cfg.SetDevice(device)
waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
cfg.SetDevice(device)
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -153,8 +139,6 @@ func (c *configMuxBuilder) registerFolder(path string) {
})
c.Handle(http.MethodDelete, path, func(w http.ResponseWriter, _ *http.Request, p httprouter.Params) {
c.mut.Lock()
defer c.mut.Unlock()
waiter, err := c.cfg.RemoveFolder(p.ByName("id"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
@ -196,8 +180,6 @@ func (c *configMuxBuilder) registerDevice(path string) {
})
c.Handle(http.MethodDelete, path, func(w http.ResponseWriter, _ *http.Request, p httprouter.Params) {
c.mut.Lock()
defer c.mut.Unlock()
id, err := protocol.DeviceIDFromString(p.ByName("id"))
waiter, err := c.cfg.RemoveDevice(id)
if err != nil {
@ -251,22 +233,27 @@ func (c *configMuxBuilder) registerGUI(path string) {
}
func (c *configMuxBuilder) adjustConfig(w http.ResponseWriter, r *http.Request) {
c.mut.Lock()
defer c.mut.Unlock()
cfg, err := config.ReadJSON(r.Body, c.id)
to, err := config.ReadJSON(r.Body, c.id)
r.Body.Close()
if err != nil {
l.Warnln("Decoding posted config:", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if cfg.GUI.Password, err = checkGUIPassword(c.cfg.GUI().Password, cfg.GUI.Password); err != nil {
l.Warnln("bcrypting password:", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
waiter, err := c.cfg.Replace(cfg)
if err != nil {
var errMsg string
var status int
waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
if to.GUI.Password, err = checkGUIPassword(cfg.GUI.Password, to.GUI.Password); err != nil {
l.Warnln("bcrypting password:", err)
errMsg = err.Error()
status = http.StatusInternalServerError
return
}
*cfg = to
})
if errMsg != "" {
http.Error(w, errMsg, status)
} else if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@ -274,13 +261,13 @@ func (c *configMuxBuilder) adjustConfig(w http.ResponseWriter, r *http.Request)
}
func (c *configMuxBuilder) adjustFolder(w http.ResponseWriter, r *http.Request, folder config.FolderConfiguration) {
c.mut.Lock()
defer c.mut.Unlock()
if err := unmarshalTo(r.Body, &folder); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
waiter, err := c.cfg.SetFolder(folder)
waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
cfg.SetFolder(folder)
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -289,13 +276,13 @@ func (c *configMuxBuilder) adjustFolder(w http.ResponseWriter, r *http.Request,
}
func (c *configMuxBuilder) adjustDevice(w http.ResponseWriter, r *http.Request, device config.DeviceConfiguration) {
c.mut.Lock()
defer c.mut.Unlock()
if err := unmarshalTo(r.Body, &device); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
waiter, err := c.cfg.SetDevice(device)
waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
cfg.SetDevice(device)
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -304,13 +291,13 @@ func (c *configMuxBuilder) adjustDevice(w http.ResponseWriter, r *http.Request,
}
func (c *configMuxBuilder) adjustOptions(w http.ResponseWriter, r *http.Request, opts config.OptionsConfiguration) {
c.mut.Lock()
defer c.mut.Unlock()
if err := unmarshalTo(r.Body, &opts); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
waiter, err := c.cfg.SetOptions(opts)
waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
cfg.Options = opts
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -319,21 +306,26 @@ func (c *configMuxBuilder) adjustOptions(w http.ResponseWriter, r *http.Request,
}
func (c *configMuxBuilder) adjustGUI(w http.ResponseWriter, r *http.Request, gui config.GUIConfiguration) {
c.mut.Lock()
defer c.mut.Unlock()
oldPassword := gui.Password
err := unmarshalTo(r.Body, &gui)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if gui.Password, err = checkGUIPassword(oldPassword, gui.Password); err != nil {
l.Warnln("bcrypting password:", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
waiter, err := c.cfg.SetGUI(gui)
if err != nil {
var errMsg string
var status int
waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
if gui.Password, err = checkGUIPassword(oldPassword, gui.Password); err != nil {
l.Warnln("bcrypting password:", err)
errMsg = err.Error()
status = http.StatusInternalServerError
return
}
cfg.GUI = gui
})
if errMsg != "" {
http.Error(w, errMsg, status)
} else if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@ -341,13 +333,13 @@ func (c *configMuxBuilder) adjustGUI(w http.ResponseWriter, r *http.Request, gui
}
func (c *configMuxBuilder) adjustLDAP(w http.ResponseWriter, r *http.Request, ldap config.LDAPConfiguration) {
c.mut.Lock()
defer c.mut.Unlock()
if err := unmarshalTo(r.Body, &ldap); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
waiter, err := c.cfg.SetLDAP(ldap)
waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
cfg.LDAP = ldap
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return

View File

@ -28,10 +28,6 @@ func (c *mockedConfig) LDAP() config.LDAPConfiguration {
return config.LDAPConfiguration{}
}
func (c *mockedConfig) SetLDAP(config.LDAPConfiguration) (config.Waiter, error) {
return noopWaiter{}, nil
}
func (c *mockedConfig) RawCopy() config.Configuration {
cfg := config.Configuration{}
util.SetDefaults(&cfg.Options)
@ -42,11 +38,13 @@ func (c *mockedConfig) Options() config.OptionsConfiguration {
return config.OptionsConfiguration{}
}
func (c *mockedConfig) Replace(cfg config.Configuration) (config.Waiter, error) {
func (c *mockedConfig) Modify(config.ModifyFunction) (config.Waiter, error) {
return noopWaiter{}, nil
}
func (c *mockedConfig) Subscribe(cm config.Committer) {}
func (c *mockedConfig) Subscribe(cm config.Committer) config.Configuration {
return config.Configuration{}
}
func (c *mockedConfig) Unsubscribe(cm config.Committer) {}
@ -62,14 +60,6 @@ func (c *mockedConfig) DeviceList() []config.DeviceConfiguration {
return nil
}
func (c *mockedConfig) SetDevice(config.DeviceConfiguration) (config.Waiter, error) {
return noopWaiter{}, nil
}
func (c *mockedConfig) SetDevices([]config.DeviceConfiguration) (config.Waiter, error) {
return noopWaiter{}, nil
}
func (c *mockedConfig) Save() error {
return nil
}
@ -86,14 +76,6 @@ func (c *mockedConfig) ConfigPath() string {
return ""
}
func (c *mockedConfig) SetGUI(gui config.GUIConfiguration) (config.Waiter, error) {
return noopWaiter{}, nil
}
func (c *mockedConfig) SetOptions(opts config.OptionsConfiguration) (config.Waiter, error) {
return noopWaiter{}, nil
}
func (c *mockedConfig) Folder(id string) (config.FolderConfiguration, bool) {
return config.FolderConfiguration{}, false
}
@ -102,14 +84,6 @@ func (c *mockedConfig) FolderList() []config.FolderConfiguration {
return nil
}
func (c *mockedConfig) SetFolder(fld config.FolderConfiguration) (config.Waiter, error) {
return noopWaiter{}, nil
}
func (c *mockedConfig) SetFolders(folders []config.FolderConfiguration) (config.Waiter, error) {
return noopWaiter{}, nil
}
func (c *mockedConfig) RemoveFolder(id string) (config.Waiter, error) {
return noopWaiter{}, nil
}

View File

@ -41,10 +41,20 @@ func (validationError) String() string {
return "validationError"
}
func TestReplaceCommit(t *testing.T) {
t.Skip("broken, fails randomly, #3834")
func replace(t testing.TB, w Wrapper, to Configuration) {
t.Helper()
waiter, err := w.Modify(func(cfg *Configuration) {
*cfg = to
})
if err != nil {
t.Fatal(err)
}
waiter.Wait()
}
func TestReplaceCommit(t *testing.T) {
w := wrap("/dev/null", Configuration{Version: 0}, device1)
defer w.stop()
if w.RawCopy().Version != 0 {
t.Fatal("Config incorrect")
}
@ -52,10 +62,7 @@ func TestReplaceCommit(t *testing.T) {
// Replace config. We should get back a clean response and the config
// should change.
_, err := w.Replace(Configuration{Version: 1})
if err != nil {
t.Fatal("Should not have a validation error:", err)
}
replace(t, w, Configuration{Version: 1})
if w.RequiresRestart() {
t.Fatal("Should not require restart")
}
@ -69,11 +76,7 @@ func TestReplaceCommit(t *testing.T) {
sub0 := requiresRestart{committed: make(chan struct{}, 1)}
w.Subscribe(sub0)
_, err = w.Replace(Configuration{Version: 2})
if err != nil {
t.Fatal("Should not have a validation error:", err)
}
replace(t, w, Configuration{Version: 1})
<-sub0.committed
if !w.RequiresRestart() {
t.Fatal("Should require restart")
@ -87,7 +90,9 @@ func TestReplaceCommit(t *testing.T) {
w.Subscribe(validationError{})
_, err = w.Replace(Configuration{Version: 3})
_, err := w.Modify(func(cfg *Configuration) {
*cfg = Configuration{Version: 3}
})
if err == nil {
t.Fatal("Should have a validation error")
}

View File

@ -372,6 +372,15 @@ func (cfg *Configuration) applyMigrations() {
migrationsMut.Unlock()
}
func (cfg *Configuration) Device(id protocol.DeviceID) (DeviceConfiguration, int, bool) {
for i, device := range cfg.Devices {
if device.DeviceID == id {
return device, i, true
}
}
return DeviceConfiguration{}, 0, false
}
// DeviceMap returns a map of device ID to device configuration for the given configuration.
func (cfg *Configuration) DeviceMap() map[protocol.DeviceID]DeviceConfiguration {
m := make(map[protocol.DeviceID]DeviceConfiguration, len(cfg.Devices))
@ -381,6 +390,44 @@ func (cfg *Configuration) DeviceMap() map[protocol.DeviceID]DeviceConfiguration
return m
}
func (cfg *Configuration) SetDevice(device DeviceConfiguration) {
cfg.SetDevices([]DeviceConfiguration{device})
}
func (cfg *Configuration) SetDevices(devices []DeviceConfiguration) {
inds := make(map[protocol.DeviceID]int, len(cfg.Devices))
for i, device := range cfg.Devices {
inds[device.DeviceID] = i
}
filtered := devices[:0]
for _, device := range devices {
if i, ok := inds[device.DeviceID]; ok {
cfg.Devices[i] = device
} else {
filtered = append(filtered, device)
}
}
cfg.Devices = append(cfg.Devices, filtered...)
}
func (cfg *Configuration) Folder(id string) (FolderConfiguration, int, bool) {
for i, folder := range cfg.Folders {
if folder.ID == id {
return folder, i, true
}
}
return FolderConfiguration{}, 0, false
}
// FolderMap returns a map of folder ID to folder configuration for the given configuration.
func (cfg *Configuration) FolderMap() map[string]FolderConfiguration {
m := make(map[string]FolderConfiguration, len(cfg.Folders))
for _, folder := range cfg.Folders {
m[folder.ID] = folder
}
return m
}
// FolderPasswords returns the folder passwords set for this device, for
// folders that have an encryption password set.
func (cfg Configuration) FolderPasswords(device protocol.DeviceID) map[string]string {
@ -397,6 +444,26 @@ nextFolder:
return res
}
func (cfg *Configuration) SetFolder(folder FolderConfiguration) {
cfg.SetFolders([]FolderConfiguration{folder})
}
func (cfg *Configuration) SetFolders(folders []FolderConfiguration) {
inds := make(map[string]int, len(cfg.Folders))
for i, folder := range cfg.Folders {
inds[folder.ID] = i
}
filtered := folders[:0]
for _, folder := range folders {
if i, ok := inds[folder.ID]; ok {
cfg.Folders[i] = folder
} else {
filtered = append(filtered, folder)
}
}
cfg.Folders = append(cfg.Folders, filtered...)
}
func ensureDevicePresent(devices []FolderDeviceConfiguration, myID protocol.DeviceID) []FolderDeviceConfiguration {
for _, device := range devices {
if device.DeviceID.Equals(myID) {

View File

@ -8,9 +8,11 @@ package config
import (
"bytes"
"context"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
@ -21,6 +23,7 @@ import (
"testing"
"github.com/d4l3k/messagediff"
"github.com/thejerf/suture/v4"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/fs"
@ -95,7 +98,8 @@ func TestDeviceConfig(t *testing.T) {
}
os.RemoveAll(filepath.Join("testdata", DefaultMarkerName))
wr, err := load(cfgFile, device1)
wr, wrCancel, err := copyAndLoad(cfgFile, device1)
defer wrCancel()
if err != nil {
t.Fatal(err)
}
@ -107,7 +111,7 @@ func TestDeviceConfig(t *testing.T) {
t.Fatal("Unexpected file")
}
cfg := wr.(*wrapper).cfg
cfg := wr.Wrapper.(*wrapper).cfg
expectedFolders := []FolderConfiguration{
{
@ -170,7 +174,8 @@ func TestDeviceConfig(t *testing.T) {
}
func TestNoListenAddresses(t *testing.T) {
cfg, err := load("testdata/nolistenaddress.xml", device1)
cfg, cfgCancel, err := copyAndLoad("testdata/nolistenaddress.xml", device1)
defer cfgCancel()
if err != nil {
t.Error(err)
}
@ -228,7 +233,8 @@ func TestOverriddenValues(t *testing.T) {
}
os.Unsetenv("STNOUPGRADE")
cfg, err := load("testdata/overridenvalues.xml", device1)
cfg, cfgCancel, err := copyAndLoad("testdata/overridenvalues.xml", device1)
defer cfgCancel()
if err != nil {
t.Error(err)
}
@ -269,7 +275,8 @@ func TestDeviceAddressesDynamic(t *testing.T) {
},
}
cfg, err := load("testdata/deviceaddressesdynamic.xml", device4)
cfg, cfgCancel, err := copyAndLoad("testdata/deviceaddressesdynamic.xml", device4)
defer cfgCancel()
if err != nil {
t.Error(err)
}
@ -314,7 +321,8 @@ func TestDeviceCompression(t *testing.T) {
},
}
cfg, err := load("testdata/devicecompression.xml", device4)
cfg, cfgCancel, err := copyAndLoad("testdata/devicecompression.xml", device4)
defer cfgCancel()
if err != nil {
t.Error(err)
}
@ -356,7 +364,8 @@ func TestDeviceAddressesStatic(t *testing.T) {
},
}
cfg, err := load("testdata/deviceaddressesstatic.xml", device4)
cfg, cfgCancel, err := copyAndLoad("testdata/deviceaddressesstatic.xml", device4)
defer cfgCancel()
if err != nil {
t.Error(err)
}
@ -368,7 +377,8 @@ func TestDeviceAddressesStatic(t *testing.T) {
}
func TestVersioningConfig(t *testing.T) {
cfg, err := load("testdata/versioningconfig.xml", device4)
cfg, cfgCancel, err := copyAndLoad("testdata/versioningconfig.xml", device4)
defer cfgCancel()
if err != nil {
t.Error(err)
}
@ -395,7 +405,8 @@ func TestIssue1262(t *testing.T) {
t.Skipf("path gets converted to absolute as part of the filesystem initialization on linux")
}
cfg, err := load("testdata/issue-1262.xml", device4)
cfg, cfgCancel, err := copyAndLoad("testdata/issue-1262.xml", device4)
defer cfgCancel()
if err != nil {
t.Fatal(err)
}
@ -409,7 +420,8 @@ func TestIssue1262(t *testing.T) {
}
func TestIssue1750(t *testing.T) {
cfg, err := load("testdata/issue-1750.xml", device4)
cfg, cfgCancel, err := copyAndLoad("testdata/issue-1750.xml", device4)
defer cfgCancel()
if err != nil {
t.Fatal(err)
}
@ -505,6 +517,7 @@ func TestFolderCheckPath(t *testing.T) {
func TestNewSaveLoad(t *testing.T) {
path := "testdata/temp.xml"
os.Remove(path)
defer os.Remove(path)
exists := func(path string) bool {
_, err := os.Stat(path)
@ -513,6 +526,7 @@ func TestNewSaveLoad(t *testing.T) {
intCfg := New(device1)
cfg := wrap(path, intCfg, device1)
defer cfg.stop()
if exists(path) {
t.Error(path, "exists")
@ -527,6 +541,7 @@ func TestNewSaveLoad(t *testing.T) {
}
cfg2, err := load(path, device1)
defer cfg2.stop()
if err != nil {
t.Error(err)
}
@ -534,8 +549,6 @@ func TestNewSaveLoad(t *testing.T) {
if diff, equal := messagediff.PrettyDiff(cfg.RawCopy(), cfg2.RawCopy()); !equal {
t.Errorf("Configs are not equal. Diff:\n%s", diff)
}
os.Remove(path)
}
func TestPrepare(t *testing.T) {
@ -553,7 +566,8 @@ func TestPrepare(t *testing.T) {
}
func TestCopy(t *testing.T) {
wrapper, err := load("testdata/example.xml", device1)
wrapper, wrapperCancel, err := copyAndLoad("testdata/example.xml", device1)
defer wrapperCancel()
if err != nil {
t.Fatal(err)
}
@ -592,7 +606,8 @@ func TestCopy(t *testing.T) {
}
func TestPullOrder(t *testing.T) {
wrapper, err := load("testdata/pullorder.xml", device1)
wrapper, wrapperCleanup, err := copyAndLoad("testdata/pullorder.xml", device1)
defer wrapperCleanup()
if err != nil {
t.Fatal(err)
}
@ -632,8 +647,9 @@ func TestPullOrder(t *testing.T) {
if err != nil {
t.Fatal(err)
}
wrapper = wrap("testdata/pullorder.xml", cfg, device1)
folders = wrapper.Folders()
wrapper2 := wrap(wrapper.ConfigPath(), cfg, device1)
defer wrapper2.stop()
folders = wrapper2.Folders()
for _, tc := range expected {
if actual := folders[tc.name].Order; actual != tc.order {
@ -643,7 +659,8 @@ func TestPullOrder(t *testing.T) {
}
func TestLargeRescanInterval(t *testing.T) {
wrapper, err := load("testdata/largeinterval.xml", device1)
wrapper, wrapperCancel, err := copyAndLoad("testdata/largeinterval.xml", device1)
defer wrapperCancel()
if err != nil {
t.Fatal(err)
}
@ -681,7 +698,8 @@ func TestGUIConfigURL(t *testing.T) {
func TestDuplicateDevices(t *testing.T) {
// Duplicate devices should be removed
wrapper, err := load("testdata/dupdevices.xml", device1)
wrapper, wrapperCancel, err := copyAndLoad("testdata/dupdevices.xml", device1)
defer wrapperCancel()
if err != nil {
t.Fatal(err)
}
@ -699,7 +717,8 @@ func TestDuplicateDevices(t *testing.T) {
func TestDuplicateFolders(t *testing.T) {
// Duplicate folders are a loading error
_, err := load("testdata/dupfolders.xml", device1)
_, _Cancel, err := copyAndLoad("testdata/dupfolders.xml", device1)
defer _Cancel()
if err == nil || !strings.Contains(err.Error(), errFolderIDDuplicate.Error()) {
t.Fatal(`Expected error to mention "duplicate folder ID":`, err)
}
@ -710,7 +729,8 @@ func TestEmptyFolderPaths(t *testing.T) {
// get messed up by the prepare steps (e.g., become the current dir or
// get a slash added so that it becomes the root directory or similar).
_, err := load("testdata/nopath.xml", device1)
_, _Cancel, err := copyAndLoad("testdata/nopath.xml", device1)
defer _Cancel()
if err == nil || !strings.Contains(err.Error(), errFolderPathEmpty.Error()) {
t.Fatal("Expected error due to empty folder path, got", err)
}
@ -779,7 +799,8 @@ func TestIgnoredDevices(t *testing.T) {
// Verify that ignored devices that are also present in the
// configuration are not in fact ignored.
wrapper, err := load("testdata/ignoreddevices.xml", device1)
wrapper, wrapperCancel, err := copyAndLoad("testdata/ignoreddevices.xml", device1)
defer wrapperCancel()
if err != nil {
t.Fatal(err)
}
@ -797,7 +818,8 @@ func TestIgnoredFolders(t *testing.T) {
// configuration are not in fact ignored.
// Also, verify that folders that are shared with a device are not ignored.
wrapper, err := load("testdata/ignoredfolders.xml", device1)
wrapper, wrapperCancel, err := copyAndLoad("testdata/ignoredfolders.xml", device1)
defer wrapperCancel()
if err != nil {
t.Fatal(err)
}
@ -833,7 +855,8 @@ func TestIgnoredFolders(t *testing.T) {
func TestGetDevice(t *testing.T) {
// Verify that the Device() call does the right thing
wrapper, err := load("testdata/ignoreddevices.xml", device1)
wrapper, wrapperCancel, err := copyAndLoad("testdata/ignoreddevices.xml", device1)
defer wrapperCancel()
if err != nil {
t.Fatal(err)
}
@ -860,7 +883,8 @@ func TestGetDevice(t *testing.T) {
}
func TestSharesRemovedOnDeviceRemoval(t *testing.T) {
wrapper, err := load("testdata/example.xml", device1)
wrapper, wrapperCancel, err := copyAndLoad("testdata/example.xml", device1)
defer wrapperCancel()
if err != nil {
t.Errorf("Failed: %s", err)
}
@ -872,10 +896,7 @@ func TestSharesRemovedOnDeviceRemoval(t *testing.T) {
t.Error("Should have less devices")
}
_, err = wrapper.Replace(raw)
if err != nil {
t.Errorf("Failed: %s", err)
}
replace(t, wrapper, raw)
raw = wrapper.RawCopy()
if len(raw.Folders[0].Devices) > len(raw.Devices) {
@ -947,6 +968,7 @@ func TestIssue4219(t *testing.T) {
}
w := wrap("/tmp/cfg", cfg, myID)
defer w.stop()
if !w.IgnoredFolder(device2, "t1") {
t.Error("Folder device2 t1 should be ignored")
}
@ -1157,13 +1179,80 @@ func defaultConfigAsMap() map[string]interface{} {
return tmp
}
func load(path string, myID protocol.DeviceID) (Wrapper, error) {
cfg, _, err := Load(path, myID, events.NoopLogger)
return cfg, err
func copyToTmp(path string) (string, error) {
orig, err := os.Open(path)
if err != nil {
return "", err
}
defer orig.Close()
temp, err := ioutil.TempFile("", "syncthing-configTest-")
if err != nil {
return "", err
}
defer temp.Close()
if _, err := io.Copy(temp, orig); err != nil {
return "", err
}
return temp.Name(), nil
}
func wrap(path string, cfg Configuration, myID protocol.DeviceID) Wrapper {
return Wrap(path, cfg, myID, events.NoopLogger)
func copyAndLoad(path string, myID protocol.DeviceID) (*testWrapper, func(), error) {
temp, err := copyToTmp(path)
if err != nil {
return nil, func() {}, err
}
wrapper, err := load(temp, myID)
if err != nil {
return nil, func() {}, err
}
return wrapper, func() {
wrapper.stop()
os.Remove(temp)
}, nil
}
func load(path string, myID protocol.DeviceID) (*testWrapper, error) {
cfg, _, err := Load(path, myID, events.NoopLogger)
if err != nil {
return nil, err
}
return startWrapper(cfg), nil
}
func wrap(path string, cfg Configuration, myID protocol.DeviceID) *testWrapper {
wrapper := Wrap(path, cfg, myID, events.NoopLogger)
return startWrapper(wrapper)
}
type testWrapper struct {
Wrapper
cancel context.CancelFunc
done chan struct{}
}
func (w *testWrapper) stop() {
w.cancel()
<-w.done
}
func startWrapper(wrapper Wrapper) *testWrapper {
tw := &testWrapper{
Wrapper: wrapper,
done: make(chan struct{}),
}
s, ok := wrapper.(suture.Service)
if !ok {
tw.cancel = func() {}
close(tw.done)
return tw
}
ctx, cancel := context.WithCancel(context.Background())
tw.cancel = cancel
go func() {
s.Serve(ctx)
close(tw.done)
}()
return tw
}
func TestInternalVersioningConfiguration(t *testing.T) {

View File

@ -7,8 +7,12 @@
package config
import (
"context"
"errors"
"os"
"reflect"
"sync/atomic"
"time"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/osutil"
@ -16,6 +20,13 @@ import (
"github.com/syncthing/syncthing/lib/sync"
)
const (
maxModifications = 1000
minSaveInterval = 5 * time.Second
)
var errTooManyModifications = errors.New("too many concurrent config modifications")
// The Committer interface is implemented by objects that need to know about
// or have a say in configuration changes.
//
@ -35,6 +46,10 @@ import (
// false will result in a "restart needed" response to the API/user. Note that
// the new configuration will still have been applied by those who were
// capable of doing so.
//
// A Committer must take care not to hold any locks while changing the
// configuration (e.g. calling Wrapper.SetFolder), that are also acquired in any
// methods of the Committer interface.
type Committer interface {
VerifyConfiguration(from, to Configuration) error
CommitConfiguration(from, to Configuration) (handled bool)
@ -50,45 +65,47 @@ type noopWaiter struct{}
func (noopWaiter) Wait() {}
// A Wrapper around a Configuration that manages loads, saves and published
// notifications of changes to registered Handlers
// ModifyFunction gets a pointer to a copy of the currently active configuration
// for modification.
type ModifyFunction func(*Configuration)
// Wrapper handles a Configuration, i.e. it provides methods to access, change
// and save the config, and notifies registered subscribers (Committer) of
// changes.
//
// Modify allows changing the currently active configuration through the given
// ModifyFunction. It can be called concurrently: All calls will be queued and
// called in order.
type Wrapper interface {
ConfigPath() string
MyID() protocol.DeviceID
RawCopy() Configuration
Replace(cfg Configuration) (Waiter, error)
RequiresRestart() bool
Save() error
GUI() GUIConfiguration
SetGUI(gui GUIConfiguration) (Waiter, error)
LDAP() LDAPConfiguration
SetLDAP(ldap LDAPConfiguration) (Waiter, error)
Modify(ModifyFunction) (Waiter, error)
RemoveFolder(id string) (Waiter, error)
RemoveDevice(id protocol.DeviceID) (Waiter, error)
GUI() GUIConfiguration
LDAP() LDAPConfiguration
Options() OptionsConfiguration
SetOptions(opts OptionsConfiguration) (Waiter, error)
Folder(id string) (FolderConfiguration, bool)
Folders() map[string]FolderConfiguration
FolderList() []FolderConfiguration
RemoveFolder(id string) (Waiter, error)
SetFolder(fld FolderConfiguration) (Waiter, error)
SetFolders(folders []FolderConfiguration) (Waiter, error)
FolderPasswords(device protocol.DeviceID) map[string]string
Device(id protocol.DeviceID) (DeviceConfiguration, bool)
Devices() map[protocol.DeviceID]DeviceConfiguration
DeviceList() []DeviceConfiguration
RemoveDevice(id protocol.DeviceID) (Waiter, error)
SetDevice(DeviceConfiguration) (Waiter, error)
SetDevices([]DeviceConfiguration) (Waiter, error)
IgnoredDevices() []ObservedDevice
IgnoredDevice(id protocol.DeviceID) bool
IgnoredFolder(device protocol.DeviceID, folder string) bool
Subscribe(c Committer)
Subscribe(c Committer) Configuration
Unsubscribe(c Committer)
}
@ -97,6 +114,7 @@ type wrapper struct {
path string
evLogger events.Logger
myID protocol.DeviceID
queue chan modifyEntry
waiter Waiter // Latest ongoing config change
subs []Committer
@ -107,12 +125,15 @@ type wrapper struct {
// Wrap wraps an existing Configuration structure and ties it to a file on
// disk.
// The returned Wrapper is a suture.Service, thus needs to be started (added to
// a supervisor).
func Wrap(path string, cfg Configuration, myID protocol.DeviceID, evLogger events.Logger) Wrapper {
w := &wrapper{
cfg: cfg,
path: path,
evLogger: evLogger,
myID: myID,
queue: make(chan modifyEntry, maxModifications),
waiter: noopWaiter{}, // Noop until first config change
mut: sync.NewMutex(),
}
@ -121,6 +142,8 @@ func Wrap(path string, cfg Configuration, myID protocol.DeviceID, evLogger event
// Load loads an existing file on disk and returns a new configuration
// wrapper.
// The returned Wrapper is a suture.Service, thus needs to be started (added to
// a supervisor).
func Load(path string, myID protocol.DeviceID, evLogger events.Logger) (Wrapper, int, error) {
fd, err := os.Open(path)
if err != nil {
@ -145,11 +168,13 @@ func (w *wrapper) MyID() protocol.DeviceID {
}
// Subscribe registers the given handler to be called on any future
// configuration changes.
func (w *wrapper) Subscribe(c Committer) {
// configuration changes. It returns the config that is in effect while
// subscribing, that can be used for initial setup.
func (w *wrapper) Subscribe(c Committer) Configuration {
w.mut.Lock()
defer w.mut.Unlock()
w.subs = append(w.subs, c)
w.mut.Unlock()
return w.cfg.Copy()
}
// Unsubscribe de-registers the given handler from any future calls to
@ -179,11 +204,84 @@ func (w *wrapper) RawCopy() Configuration {
return w.cfg.Copy()
}
// Replace swaps the current configuration object for the given one.
func (w *wrapper) Replace(cfg Configuration) (Waiter, error) {
w.mut.Lock()
defer w.mut.Unlock()
return w.replaceLocked(cfg.Copy())
func (w *wrapper) Modify(fn ModifyFunction) (Waiter, error) {
return w.modifyQueued(fn)
}
func (w *wrapper) modifyQueued(modifyFunc ModifyFunction) (Waiter, error) {
e := modifyEntry{
modifyFunc: modifyFunc,
res: make(chan modifyResult),
}
select {
case w.queue <- e:
default:
return noopWaiter{}, errTooManyModifications
}
res := <-e.res
return res.w, res.err
}
func (w *wrapper) Serve(ctx context.Context) error {
defer w.serveSave()
var e modifyEntry
saveTimer := time.NewTimer(0)
<-saveTimer.C
saveTimerRunning := false
for {
select {
case e = <-w.queue:
case <-saveTimer.C:
w.serveSave()
saveTimerRunning = false
continue
case <-ctx.Done():
return ctx.Err()
}
var waiter Waiter = noopWaiter{}
var err error
// Let the caller modify the config.
to := w.RawCopy()
e.modifyFunc(&to)
// Check if the config was actually changed at all.
w.mut.Lock()
if !reflect.DeepEqual(w.cfg, to) {
waiter, err = w.replaceLocked(to)
if !saveTimerRunning {
saveTimer.Reset(minSaveInterval)
saveTimerRunning = true
}
}
w.mut.Unlock()
e.res <- modifyResult{
w: waiter,
err: err,
}
// Wait for all subscriber to handle the config change before continuing
// to process the next change.
done := make(chan struct{})
go func() {
waiter.Wait()
close(done)
}()
select {
case <-done:
case <-ctx.Done():
return ctx.Err()
}
}
}
func (w *wrapper) serveSave() {
if err := w.Save(); err != nil {
l.Warnln("Failed to save config:", err)
}
}
func (w *wrapper) replaceLocked(to Configuration) (Waiter, error) {
@ -246,55 +344,16 @@ func (w *wrapper) DeviceList() []DeviceConfiguration {
return w.cfg.Copy().Devices
}
// SetDevices adds new devices to the configuration, or overwrites existing
// devices with the same ID.
func (w *wrapper) SetDevices(devs []DeviceConfiguration) (Waiter, error) {
w.mut.Lock()
defer w.mut.Unlock()
newCfg := w.cfg.Copy()
var replaced bool
for oldIndex := range devs {
replaced = false
for newIndex := range newCfg.Devices {
if newCfg.Devices[newIndex].DeviceID == devs[oldIndex].DeviceID {
newCfg.Devices[newIndex] = devs[oldIndex].Copy()
replaced = true
break
}
}
if !replaced {
newCfg.Devices = append(newCfg.Devices, devs[oldIndex].Copy())
}
}
return w.replaceLocked(newCfg)
}
// SetDevice adds a new device to the configuration, or overwrites an existing
// device with the same ID.
func (w *wrapper) SetDevice(dev DeviceConfiguration) (Waiter, error) {
return w.SetDevices([]DeviceConfiguration{dev})
}
// RemoveDevice removes the device from the configuration
func (w *wrapper) RemoveDevice(id protocol.DeviceID) (Waiter, error) {
w.mut.Lock()
defer w.mut.Unlock()
newCfg := w.cfg.Copy()
for i := range newCfg.Devices {
if newCfg.Devices[i].DeviceID == id {
newCfg.Devices = append(newCfg.Devices[:i], newCfg.Devices[i+1:]...)
return w.replaceLocked(newCfg)
return w.modifyQueued(func(cfg *Configuration) {
if _, i, ok := cfg.Device(id); ok {
cfg.Devices = append(cfg.Devices[:i], cfg.Devices[i+1:]...)
}
}
return noopWaiter{}, nil
})
}
// Folders returns a map of folders. Folder structures should not be changed,
// other than for the purpose of updating via SetFolder().
// Folders returns a map of folders.
func (w *wrapper) Folders() map[string]FolderConfiguration {
w.mut.Lock()
defer w.mut.Unlock()
@ -312,51 +371,13 @@ func (w *wrapper) FolderList() []FolderConfiguration {
return w.cfg.Copy().Folders
}
// SetFolder adds a new folder to the configuration, or overwrites an existing
// folder with the same ID.
func (w *wrapper) SetFolder(fld FolderConfiguration) (Waiter, error) {
return w.SetFolders([]FolderConfiguration{fld})
}
// SetFolders adds new folders to the configuration, or overwrites existing
// folders with the same ID.
func (w *wrapper) SetFolders(folders []FolderConfiguration) (Waiter, error) {
w.mut.Lock()
defer w.mut.Unlock()
newCfg := w.cfg.Copy()
inds := make(map[string]int, len(w.cfg.Folders))
for i, folder := range newCfg.Folders {
inds[folder.ID] = i
}
filtered := folders[:0]
for _, folder := range folders {
if i, ok := inds[folder.ID]; ok {
newCfg.Folders[i] = folder
} else {
filtered = append(filtered, folder)
}
}
newCfg.Folders = append(newCfg.Folders, filtered...)
return w.replaceLocked(newCfg)
}
// RemoveFolder removes the folder from the configuration
func (w *wrapper) RemoveFolder(id string) (Waiter, error) {
w.mut.Lock()
defer w.mut.Unlock()
newCfg := w.cfg.Copy()
for i := range newCfg.Folders {
if newCfg.Folders[i].ID == id {
newCfg.Folders = append(newCfg.Folders[:i], newCfg.Folders[i+1:]...)
return w.replaceLocked(newCfg)
return w.modifyQueued(func(cfg *Configuration) {
if _, i, ok := cfg.Folder(id); ok {
cfg.Folders = append(cfg.Folders[:i], cfg.Folders[i+1:]...)
}
}
return noopWaiter{}, nil
})
}
// FolderPasswords returns the folder passwords set for this device, for
@ -374,29 +395,12 @@ func (w *wrapper) Options() OptionsConfiguration {
return w.cfg.Options.Copy()
}
// SetOptions replaces the current options configuration object.
func (w *wrapper) SetOptions(opts OptionsConfiguration) (Waiter, error) {
w.mut.Lock()
defer w.mut.Unlock()
newCfg := w.cfg.Copy()
newCfg.Options = opts.Copy()
return w.replaceLocked(newCfg)
}
func (w *wrapper) LDAP() LDAPConfiguration {
w.mut.Lock()
defer w.mut.Unlock()
return w.cfg.LDAP.Copy()
}
func (w *wrapper) SetLDAP(ldap LDAPConfiguration) (Waiter, error) {
w.mut.Lock()
defer w.mut.Unlock()
newCfg := w.cfg.Copy()
newCfg.LDAP = ldap.Copy()
return w.replaceLocked(newCfg)
}
// GUI returns the current GUI configuration object.
func (w *wrapper) GUI() GUIConfiguration {
w.mut.Lock()
@ -404,15 +408,6 @@ func (w *wrapper) GUI() GUIConfiguration {
return w.cfg.GUI.Copy()
}
// SetGUI replaces the current GUI configuration object.
func (w *wrapper) SetGUI(gui GUIConfiguration) (Waiter, error) {
w.mut.Lock()
defer w.mut.Unlock()
newCfg := w.cfg.Copy()
newCfg.GUI = gui.Copy()
return w.replaceLocked(newCfg)
}
// IgnoredDevice returns whether or not connection attempts from the given
// device should be silently ignored.
func (w *wrapper) IgnoredDevice(id protocol.DeviceID) bool {
@ -449,24 +444,22 @@ func (w *wrapper) IgnoredFolder(device protocol.DeviceID, folder string) bool {
func (w *wrapper) Device(id protocol.DeviceID) (DeviceConfiguration, bool) {
w.mut.Lock()
defer w.mut.Unlock()
for _, device := range w.cfg.Devices {
if device.DeviceID == id {
return device.Copy(), true
}
device, _, ok := w.cfg.Device(id)
if !ok {
return DeviceConfiguration{}, false
}
return DeviceConfiguration{}, false
return device.Copy(), ok
}
// Folder returns the configuration for the given folder and an "ok" bool.
func (w *wrapper) Folder(id string) (FolderConfiguration, bool) {
w.mut.Lock()
defer w.mut.Unlock()
for _, folder := range w.cfg.Folders {
if folder.ID == id {
return folder.Copy(), true
}
fcfg, _, ok := w.cfg.Folder(id)
if !ok {
return FolderConfiguration{}, false
}
return FolderConfiguration{}, false
return fcfg.Copy(), ok
}
// Save writes the configuration to disk, and generates a ConfigSaved event.
@ -502,3 +495,13 @@ func (w *wrapper) RequiresRestart() bool {
func (w *wrapper) setRequiresRestart() {
atomic.StoreUint32(&w.requiresRestart, 1)
}
type modifyEntry struct {
modifyFunc ModifyFunction
res chan modifyResult
}
type modifyResult struct {
w Waiter
err error
}

View File

@ -8,6 +8,7 @@ package connections
import (
"bytes"
"context"
crand "crypto/rand"
"io"
"math/rand"
@ -16,6 +17,7 @@ import (
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/thejerf/suture/v4"
"golang.org/x/time/rate"
)
@ -29,24 +31,34 @@ func init() {
device4, _ = protocol.DeviceIDFromString("P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2")
}
func initConfig() config.Wrapper {
cfg := config.Wrap("/dev/null", config.New(device1), device1, events.NoopLogger)
func initConfig() (config.Wrapper, context.CancelFunc) {
wrapper := config.Wrap("/dev/null", config.New(device1), device1, events.NoopLogger)
dev1Conf = config.NewDeviceConfiguration(device1, "device1")
dev2Conf = config.NewDeviceConfiguration(device2, "device2")
dev3Conf = config.NewDeviceConfiguration(device3, "device3")
dev4Conf = config.NewDeviceConfiguration(device4, "device4")
var cancel context.CancelFunc = func() {}
if wrapperService, ok := wrapper.(suture.Service); ok {
var ctx context.Context
ctx, cancel = context.WithCancel(context.Background())
go wrapperService.Serve(ctx)
}
dev2Conf.MaxRecvKbps = rand.Int() % 100000
dev2Conf.MaxSendKbps = rand.Int() % 100000
waiter, _ := cfg.SetDevices([]config.DeviceConfiguration{dev1Conf, dev2Conf, dev3Conf, dev4Conf})
waiter, _ := wrapper.Modify(func(cfg *config.Configuration) {
cfg.SetDevices([]config.DeviceConfiguration{dev1Conf, dev2Conf, dev3Conf, dev4Conf})
})
waiter.Wait()
return cfg
return wrapper, cancel
}
func TestLimiterInit(t *testing.T) {
cfg := initConfig()
lim := newLimiter(device1, cfg)
wrapper, wrapperCancel := initConfig()
defer wrapperCancel()
lim := newLimiter(device1, wrapper)
device2ReadLimit := dev2Conf.MaxRecvKbps
device2WriteLimit := dev2Conf.MaxSendKbps
@ -70,8 +82,9 @@ func TestLimiterInit(t *testing.T) {
}
func TestSetDeviceLimits(t *testing.T) {
cfg := initConfig()
lim := newLimiter(device1, cfg)
wrapper, wrapperCancel := initConfig()
defer wrapperCancel()
lim := newLimiter(device1, wrapper)
// should still be inf/inf because this is local device
dev1ReadLimit := rand.Int() % 100000
@ -87,7 +100,9 @@ func TestSetDeviceLimits(t *testing.T) {
dev3ReadLimit := rand.Int() % 10000
dev3Conf.MaxRecvKbps = dev3ReadLimit
waiter, _ := cfg.SetDevices([]config.DeviceConfiguration{dev1Conf, dev2Conf, dev3Conf, dev4Conf})
waiter, _ := wrapper.Modify(func(cfg *config.Configuration) {
cfg.SetDevices([]config.DeviceConfiguration{dev1Conf, dev2Conf, dev3Conf, dev4Conf})
})
waiter.Wait()
expectedR := map[protocol.DeviceID]*rate.Limiter{
@ -108,10 +123,11 @@ func TestSetDeviceLimits(t *testing.T) {
}
func TestRemoveDevice(t *testing.T) {
cfg := initConfig()
lim := newLimiter(device1, cfg)
wrapper, wrapperCancel := initConfig()
defer wrapperCancel()
lim := newLimiter(device1, wrapper)
waiter, _ := cfg.RemoveDevice(device3)
waiter, _ := wrapper.RemoveDevice(device3)
waiter.Wait()
expectedR := map[protocol.DeviceID]*rate.Limiter{
device2: rate.NewLimiter(rate.Limit(dev2Conf.MaxRecvKbps*1024), limiterBurstSize),
@ -128,15 +144,18 @@ func TestRemoveDevice(t *testing.T) {
}
func TestAddDevice(t *testing.T) {
cfg := initConfig()
lim := newLimiter(device1, cfg)
wrapper, wrapperCancel := initConfig()
defer wrapperCancel()
lim := newLimiter(device1, wrapper)
addedDevice, _ := protocol.DeviceIDFromString("XZJ4UNS-ENI7QGJ-J45DT6G-QSGML2K-6I4XVOG-NAZ7BF5-2VAOWNT-TFDOMQU")
addDevConf := config.NewDeviceConfiguration(addedDevice, "addedDevice")
addDevConf.MaxRecvKbps = 120
addDevConf.MaxSendKbps = 240
waiter, _ := cfg.SetDevice(addDevConf)
waiter, _ := wrapper.Modify(func(cfg *config.Configuration) {
cfg.SetDevice(addDevConf)
})
waiter.Wait()
expectedR := map[protocol.DeviceID]*rate.Limiter{
@ -159,17 +178,20 @@ func TestAddDevice(t *testing.T) {
}
func TestAddAndRemove(t *testing.T) {
cfg := initConfig()
lim := newLimiter(device1, cfg)
wrapper, wrapperCancel := initConfig()
defer wrapperCancel()
lim := newLimiter(device1, wrapper)
addedDevice, _ := protocol.DeviceIDFromString("XZJ4UNS-ENI7QGJ-J45DT6G-QSGML2K-6I4XVOG-NAZ7BF5-2VAOWNT-TFDOMQU")
addDevConf := config.NewDeviceConfiguration(addedDevice, "addedDevice")
addDevConf.MaxRecvKbps = 120
addDevConf.MaxSendKbps = 240
waiter, _ := cfg.SetDevice(addDevConf)
waiter, _ := wrapper.Modify(func(cfg *config.Configuration) {
cfg.SetDevice(addDevConf)
})
waiter.Wait()
waiter, _ = cfg.RemoveDevice(device3)
waiter, _ = wrapper.RemoveDevice(device3)
waiter.Wait()
expectedR := map[protocol.DeviceID]*rate.Limiter{

View File

@ -25,7 +25,8 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
// Get us a model up and running
m, f := setupROFolder(t)
m, f, wcfgCancel := setupROFolder(t)
defer wcfgCancel()
ffs := f.Filesystem()
defer cleanupModel(m)
@ -105,7 +106,8 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
// Get us a model up and running
m, f := setupROFolder(t)
m, f, wcfgCancel := setupROFolder(t)
defer wcfgCancel()
ffs := f.Filesystem()
defer cleanupModel(m)
@ -193,7 +195,8 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
func TestRecvOnlyUndoChanges(t *testing.T) {
// Get us a model up and running
m, f := setupROFolder(t)
m, f, wcfgCancel := setupROFolder(t)
defer wcfgCancel()
ffs := f.Filesystem()
defer cleanupModel(m)
@ -261,7 +264,8 @@ func TestRecvOnlyUndoChanges(t *testing.T) {
func TestRecvOnlyDeletedRemoteDrop(t *testing.T) {
// Get us a model up and running
m, f := setupROFolder(t)
m, f, wcfgCancel := setupROFolder(t)
defer wcfgCancel()
ffs := f.Filesystem()
defer cleanupModel(m)
@ -324,7 +328,8 @@ func TestRecvOnlyDeletedRemoteDrop(t *testing.T) {
func TestRecvOnlyRemoteUndoChanges(t *testing.T) {
// Get us a model up and running
m, f := setupROFolder(t)
m, f, wcfgCancel := setupROFolder(t)
defer wcfgCancel()
ffs := f.Filesystem()
defer cleanupModel(m)
@ -443,17 +448,17 @@ func setupKnownFiles(t *testing.T, ffs fs.Filesystem, data []byte) []protocol.Fi
return knownFiles
}
func setupROFolder(t *testing.T) (*testModel, *receiveOnlyFolder) {
func setupROFolder(t *testing.T) (*testModel, *receiveOnlyFolder, context.CancelFunc) {
t.Helper()
w := createTmpWrapper(defaultCfg)
w, cancel := createTmpWrapper(defaultCfg)
cfg := w.RawCopy()
fcfg := testFolderConfigFake()
fcfg.ID = "ro"
fcfg.Label = "ro"
fcfg.Type = config.FolderTypeReceiveOnly
cfg.Folders = []config.FolderConfiguration{fcfg}
w.Replace(cfg)
replace(t, w, cfg)
m := newModel(t, w, myID, "syncthing", "dev", nil)
m.ServeBackground()
@ -464,7 +469,7 @@ func setupROFolder(t *testing.T) (*testModel, *receiveOnlyFolder) {
defer m.fmut.RUnlock()
f := m.folderRunners["ro"].(*receiveOnlyFolder)
return m, f
return m, f, cancel
}
func writeFile(fs fs.Filesystem, filename string, data []byte, perm fs.FileMode) error {

View File

@ -92,8 +92,8 @@ func createFile(t *testing.T, name string, fs fs.Filesystem) protocol.FileInfo {
}
// Sets up a folder and model, but makes sure the services aren't actually running.
func setupSendReceiveFolder(t testing.TB, files ...protocol.FileInfo) (*testModel, *sendReceiveFolder) {
w, fcfg := tmpDefaultWrapper()
func setupSendReceiveFolder(t testing.TB, files ...protocol.FileInfo) (*testModel, *sendReceiveFolder, context.CancelFunc) {
w, fcfg, wCancel := tmpDefaultWrapper()
// Initialise model and stop immediately.
model := setupModel(t, w)
model.cancel()
@ -107,10 +107,11 @@ func setupSendReceiveFolder(t testing.TB, files ...protocol.FileInfo) (*testMode
f.updateLocalsFromScanning(files)
}
return model, f
return model, f, wCancel
}
func cleanupSRFolder(f *sendReceiveFolder, m *testModel) {
func cleanupSRFolder(f *sendReceiveFolder, m *testModel, wrapperCancel context.CancelFunc) {
wrapperCancel()
os.Remove(m.cfg.ConfigPath())
os.RemoveAll(f.Filesystem().URI())
}
@ -130,8 +131,8 @@ func TestHandleFile(t *testing.T) {
requiredFile := existingFile
requiredFile.Blocks = blocks[1:]
m, f := setupSendReceiveFolder(t, existingFile)
defer cleanupSRFolder(f, m)
m, f, wcfgCancel := setupSendReceiveFolder(t, existingFile)
defer cleanupSRFolder(f, m, wcfgCancel)
copyChan := make(chan copyBlocksState, 1)
@ -172,8 +173,8 @@ func TestHandleFileWithTemp(t *testing.T) {
requiredFile := existingFile
requiredFile.Blocks = blocks[1:]
m, f := setupSendReceiveFolder(t, existingFile)
defer cleanupSRFolder(f, m)
m, f, wcfgCancel := setupSendReceiveFolder(t, existingFile)
defer cleanupSRFolder(f, m, wcfgCancel)
if _, err := prepareTmpFile(f.Filesystem()); err != nil {
t.Fatal(err)
@ -228,10 +229,10 @@ func TestCopierFinder(t *testing.T) {
requiredFile.Blocks = blocks[1:]
requiredFile.Name = "file2"
m, f := setupSendReceiveFolder(t, existingFile)
m, f, wcfgCancel := setupSendReceiveFolder(t, existingFile)
f.CopyRangeMethod = method
defer cleanupSRFolder(f, m)
defer cleanupSRFolder(f, m, wcfgCancel)
if _, err := prepareTmpFile(f.Filesystem()); err != nil {
t.Fatal(err)
@ -309,8 +310,8 @@ func TestCopierFinder(t *testing.T) {
func TestWeakHash(t *testing.T) {
// Setup the model/pull environment
model, fo := setupSendReceiveFolder(t)
defer cleanupSRFolder(fo, model)
model, fo, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(fo, model, wcfgCancel)
ffs := fo.Filesystem()
tempFile := fs.TempName("weakhash")
@ -438,8 +439,8 @@ func TestCopierCleanup(t *testing.T) {
// Create a file
file := setupFile("test", []int{0})
file.Size = 1
m, f := setupSendReceiveFolder(t, file)
defer cleanupSRFolder(f, m)
m, f, wcfgCancel := setupSendReceiveFolder(t, file)
defer cleanupSRFolder(f, m, wcfgCancel)
file.Blocks = []protocol.BlockInfo{blocks[1]}
file.Version = file.Version.Update(myID.Short())
@ -471,8 +472,8 @@ func TestCopierCleanup(t *testing.T) {
func TestDeregisterOnFailInCopy(t *testing.T) {
file := setupFile("filex", []int{0, 2, 0, 0, 5, 0, 0, 8})
m, f := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m)
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
// Set up our evet subscription early
s := m.evLogger.Subscribe(events.ItemFinished)
@ -571,8 +572,8 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
func TestDeregisterOnFailInPull(t *testing.T) {
file := setupFile("filex", []int{0, 2, 0, 0, 5, 0, 0, 8})
m, f := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m)
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
// Set up our evet subscription early
s := m.evLogger.Subscribe(events.ItemFinished)
@ -674,8 +675,8 @@ func TestDeregisterOnFailInPull(t *testing.T) {
}
func TestIssue3164(t *testing.T) {
m, f := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m)
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem()
tmpDir := ffs.URI()
@ -765,8 +766,8 @@ func TestDiffEmpty(t *testing.T) {
// option is true and the permissions do not match between the file on disk and
// in the db.
func TestDeleteIgnorePerms(t *testing.T) {
m, f := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m)
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem()
f.IgnorePerms = true
@ -803,8 +804,8 @@ func TestCopyOwner(t *testing.T) {
// Set up a folder with the CopyParentOwner bit and backed by a fake
// filesystem.
m, f := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m)
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
f.folder.FolderConfiguration = config.NewFolderConfiguration(m.id, f.ID, f.Label, fs.FilesystemTypeFake, "/TestCopyOwner")
f.folder.FolderConfiguration.CopyOwnershipFromParent = true
@ -906,8 +907,8 @@ func TestCopyOwner(t *testing.T) {
// TestSRConflictReplaceFileByDir checks that a conflict is created when an existing file
// is replaced with a directory and versions are conflicting
func TestSRConflictReplaceFileByDir(t *testing.T) {
m, f := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m)
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem()
name := "foo"
@ -938,8 +939,8 @@ func TestSRConflictReplaceFileByDir(t *testing.T) {
// TestSRConflictReplaceFileByLink checks that a conflict is created when an existing file
// is replaced with a link and versions are conflicting
func TestSRConflictReplaceFileByLink(t *testing.T) {
m, f := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m)
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem()
name := "foo"
@ -971,8 +972,8 @@ func TestSRConflictReplaceFileByLink(t *testing.T) {
// TestDeleteBehindSymlink checks that we don't delete or schedule a scan
// when trying to delete a file behind a symlink.
func TestDeleteBehindSymlink(t *testing.T) {
m, f := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m)
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem()
destDir := createTmpDir()
@ -1022,8 +1023,8 @@ func TestDeleteBehindSymlink(t *testing.T) {
// Reproduces https://github.com/syncthing/syncthing/issues/6559
func TestPullCtxCancel(t *testing.T) {
m, f := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m)
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
pullChan := make(chan pullBlockState)
finisherChan := make(chan *sharedPullerState)
@ -1064,8 +1065,8 @@ func TestPullCtxCancel(t *testing.T) {
}
func TestPullDeleteUnscannedDir(t *testing.T) {
m, f := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m)
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem()
dir := "foobar"
@ -1093,8 +1094,8 @@ func TestPullDeleteUnscannedDir(t *testing.T) {
}
func TestPullCaseOnlyPerformFinish(t *testing.T) {
m, f := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m)
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem()
name := "foo"
@ -1154,8 +1155,8 @@ func TestPullCaseOnlySymlink(t *testing.T) {
}
func testPullCaseOnlyDirOrSymlink(t *testing.T, dir bool) {
m, f := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m)
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem()
name := "foo"
@ -1209,8 +1210,8 @@ func testPullCaseOnlyDirOrSymlink(t *testing.T, dir bool) {
}
func TestPullTempFileCaseConflict(t *testing.T) {
m, f := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m)
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
copyChan := make(chan copyBlocksState, 1)
@ -1235,8 +1236,8 @@ func TestPullTempFileCaseConflict(t *testing.T) {
}
func TestPullCaseOnlyRename(t *testing.T) {
m, f := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m)
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
// tempNameConfl := fs.TempName(confl)
@ -1278,8 +1279,8 @@ func TestPullSymlinkOverExistingWindows(t *testing.T) {
t.Skip()
}
m, f := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m)
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
name := "foo"
if fd, err := f.mtimefs.Create(name); err != nil {
@ -1318,8 +1319,8 @@ func TestPullSymlinkOverExistingWindows(t *testing.T) {
}
func TestPullDeleteCaseConflict(t *testing.T) {
m, f := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m)
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
name := "foo"
fi := protocol.FileInfo{Name: "Foo"}
@ -1352,8 +1353,8 @@ func TestPullDeleteCaseConflict(t *testing.T) {
}
func TestPullDeleteIgnoreChildDir(t *testing.T) {
m, f := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m)
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
parent := "parent"
del := "ignored"

View File

@ -136,6 +136,7 @@ type model struct {
// such as scans and pulls.
folderIOLimiter *byteSemaphore
fatalChan chan error
started chan struct{}
// fields protected by fmut
fmut sync.RWMutex
@ -162,7 +163,6 @@ type model struct {
// for testing only
foldersRunning int32
started chan struct{}
}
type folderFactory func(*model, *db.FileSet, *ignore.Matcher, config.FolderConfiguration, versioner.Versioner, events.Logger, *byteSemaphore) service
@ -221,6 +221,7 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio
globalRequestLimiter: newByteSemaphore(1024 * cfg.Options().MaxConcurrentIncomingRequestKiB()),
folderIOLimiter: newByteSemaphore(cfg.Options().MaxFolderConcurrency()),
fatalChan: make(chan error),
started: make(chan struct{}),
// fields protected by fmut
fmut: sync.NewRWMutex(),
@ -243,9 +244,6 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio
deviceDownloads: make(map[protocol.DeviceID]*deviceDownloadState),
remotePausedFolders: make(map[protocol.DeviceID]map[string]struct{}),
indexSenders: make(map[protocol.DeviceID]*indexSenderRegistry),
// for testing only
started: make(chan struct{}),
}
for devID := range cfg.Devices() {
m.deviceStatRefs[devID] = stats.NewDeviceStatisticsReference(m.db, devID)
@ -259,10 +257,10 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio
func (m *model) serve(ctx context.Context) error {
defer m.closeAllConnectionsAndWait()
m.cfg.Subscribe(m)
cfg := m.cfg.Subscribe(m)
defer m.cfg.Unsubscribe(m)
if err := m.initFolders(); err != nil {
if err := m.initFolders(cfg); err != nil {
close(m.started)
return svcutil.AsFatalErr(err, svcutil.ExitError)
}
@ -277,17 +275,14 @@ func (m *model) serve(ctx context.Context) error {
}
}
func (m *model) initFolders() error {
cacheIgnoredFiles := m.cfg.Options().CacheIgnoredFiles
existingDevices := m.cfg.Devices()
existingFolders := m.cfg.Folders()
clusterConfigDevices := make(deviceIDSet, len(existingDevices))
for _, folderCfg := range existingFolders {
func (m *model) initFolders(cfg config.Configuration) error {
clusterConfigDevices := make(deviceIDSet, len(cfg.Devices))
for _, folderCfg := range cfg.Folders {
if folderCfg.Paused {
folderCfg.CreateRoot()
continue
}
err := m.newFolder(folderCfg, cacheIgnoredFiles)
err := m.newFolder(folderCfg, cfg.Options.CacheIgnoredFiles)
if err != nil {
return err
}
@ -295,7 +290,7 @@ func (m *model) initFolders() error {
}
ignoredDevices := observedDeviceSet(m.cfg.IgnoredDevices())
m.cleanPending(existingDevices, existingFolders, ignoredDevices, nil)
m.cleanPending(cfg.DeviceMap(), cfg.FolderMap(), ignoredDevices, nil)
m.resendClusterConfig(clusterConfigDevices.AsSlice())
return nil
@ -1184,7 +1179,6 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
panic("bug: ClusterConfig called on closed or nonexistent connection")
}
changed := false
deviceCfg, ok := m.cfg.Device(deviceID)
if !ok {
l.Debugln("Device disappeared from config while processing cluster-config")
@ -1219,21 +1213,33 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
// Needs to happen outside of the fmut, as can cause CommitConfiguration
if deviceCfg.AutoAcceptFolders {
changedFolders := make([]config.FolderConfiguration, 0, len(cm.Folders))
for _, folder := range cm.Folders {
if fcfg, fchanged := m.handleAutoAccepts(deviceID, folder, ccDeviceInfos[folder.ID]); fchanged {
changedFolders = append(changedFolders, fcfg)
w, _ := m.cfg.Modify(func(cfg *config.Configuration) {
changedFcfg := make(map[string]config.FolderConfiguration)
haveFcfg := cfg.FolderMap()
for _, folder := range cm.Folders {
from, ok := haveFcfg[folder.ID]
if to, changed := m.handleAutoAccepts(deviceID, folder, ccDeviceInfos[folder.ID], from, ok, cfg.Options.DefaultFolderPath); changed {
changedFcfg[folder.ID] = to
}
}
}
if len(changedFolders) > 0 {
// Need to wait for the waiter, as this calls CommitConfiguration,
// which sets up the folder and as we return from this call,
// ClusterConfig starts poking at m.folderFiles and other things
// that might not exist until the config is committed.
w, _ := m.cfg.SetFolders(changedFolders)
w.Wait()
changed = true
}
if len(changedFcfg) == 0 {
return
}
for i := range cfg.Folders {
if fcfg, ok := changedFcfg[cfg.Folders[i].ID]; ok {
cfg.Folders[i] = fcfg
delete(changedFcfg, cfg.Folders[i].ID)
}
}
for _, fcfg := range changedFcfg {
cfg.Folders = append(cfg.Folders, fcfg)
}
})
// Need to wait for the waiter, as this calls CommitConfiguration,
// which sets up the folder and as we return from this call,
// ClusterConfig starts poking at m.folderFiles and other things
// that might not exist until the config is committed.
w.Wait()
}
tempIndexFolders, paused, err := m.ccHandleFolders(cm.Folders, deviceCfg, ccDeviceInfos, indexSenderRegistry)
@ -1257,11 +1263,12 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
}
if deviceCfg.Introducer {
folders, devices, foldersDevices, introduced := m.handleIntroductions(deviceCfg, cm)
folders, devices, deintroduced := m.handleDeintroductions(deviceCfg, foldersDevices, folders, devices)
if introduced || deintroduced {
changed = true
cfg := m.cfg.RawCopy()
m.cfg.Modify(func(cfg *config.Configuration) {
folders, devices, foldersDevices, introduced := m.handleIntroductions(deviceCfg, cm, cfg.FolderMap(), cfg.DeviceMap())
folders, devices, deintroduced := m.handleDeintroductions(deviceCfg, foldersDevices, folders, devices)
if !introduced && !deintroduced {
return
}
cfg.Folders = make([]config.FolderConfiguration, 0, len(folders))
for _, fcfg := range folders {
cfg.Folders = append(cfg.Folders, fcfg)
@ -1270,14 +1277,7 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
for _, dcfg := range devices {
cfg.Devices = append(cfg.Devices, dcfg)
}
m.cfg.Replace(cfg)
}
}
if changed {
if err := m.cfg.Save(); err != nil {
l.Warnln("Failed to save config", err)
}
})
}
return nil
@ -1492,10 +1492,8 @@ func (m *model) resendClusterConfig(ids []protocol.DeviceID) {
}
// handleIntroductions handles adding devices/folders that are shared by an introducer device
func (m *model) handleIntroductions(introducerCfg config.DeviceConfiguration, cm protocol.ClusterConfig) (map[string]config.FolderConfiguration, map[protocol.DeviceID]config.DeviceConfiguration, folderDeviceSet, bool) {
func (m *model) handleIntroductions(introducerCfg config.DeviceConfiguration, cm protocol.ClusterConfig, folders map[string]config.FolderConfiguration, devices map[protocol.DeviceID]config.DeviceConfiguration) (map[string]config.FolderConfiguration, map[protocol.DeviceID]config.DeviceConfiguration, folderDeviceSet, bool) {
changed := false
folders := m.cfg.Folders()
devices := m.cfg.Devices()
foldersDevices := make(folderDeviceSet)
@ -1521,7 +1519,7 @@ func (m *model) handleIntroductions(introducerCfg config.DeviceConfiguration, cm
foldersDevices.set(device.ID, folder.ID)
if _, ok := m.cfg.Device(device.ID); !ok {
if _, ok := devices[device.ID]; !ok {
// The device is currently unknown. Add it to the config.
devices[device.ID] = m.introduceDevice(device, introducerCfg)
} else if fcfg.SharedWith(device.ID) {
@ -1602,9 +1600,8 @@ func (m *model) handleDeintroductions(introducerCfg config.DeviceConfiguration,
// handleAutoAccepts handles adding and sharing folders for devices that have
// AutoAcceptFolders set to true.
func (m *model) handleAutoAccepts(deviceID protocol.DeviceID, folder protocol.Folder, ccDeviceInfos *indexSenderStartInfo) (config.FolderConfiguration, bool) {
if cfg, ok := m.cfg.Folder(folder.ID); !ok {
defaultPath := m.cfg.Options().DefaultFolderPath
func (m *model) handleAutoAccepts(deviceID protocol.DeviceID, folder protocol.Folder, ccDeviceInfos *indexSenderStartInfo, cfg config.FolderConfiguration, haveCfg bool, defaultPath string) (config.FolderConfiguration, bool) {
if !haveCfg {
defaultPathFs := fs.NewFilesystem(fs.FilesystemTypeBasic, defaultPath)
pathAlternatives := []string{
fs.SanitizePath(folder.Label),
@ -2179,9 +2176,16 @@ func (m *model) AddConnection(conn protocol.Connection, hello protocol.Hello) {
conn.ClusterConfig(cm)
if (device.Name == "" || m.cfg.Options().OverwriteRemoteDevNames) && hello.DeviceName != "" {
device.Name = hello.DeviceName
m.cfg.SetDevice(device)
m.cfg.Save()
m.cfg.Modify(func(cfg *config.Configuration) {
for i := range cfg.Devices {
if cfg.Devices[i].DeviceID == deviceID {
if cfg.Devices[i].Name == "" || cfg.Options.OverwriteRemoteDevNames {
cfg.Devices[i].Name = hello.DeviceName
}
return
}
}
})
}
m.deviceWasSeen(deviceID)
@ -2651,6 +2655,9 @@ func (m *model) VerifyConfiguration(from, to config.Configuration) error {
func (m *model) CommitConfiguration(from, to config.Configuration) bool {
// TODO: This should not use reflect, and should take more care to try to handle stuff without restart.
// Delay processing config changes until after the initial setup
<-m.started
// Go through the folder configs and figure out if we need to restart or not.
// Tracks devices affected by any configuration change to resend ClusterConfig.

File diff suppressed because it is too large Load Diff

View File

@ -60,11 +60,16 @@ func TestProgressEmitter(t *testing.T) {
w := evLogger.Subscribe(events.DownloadProgress)
c := createTmpWrapper(config.Configuration{})
c, cfgCancel := createTmpWrapper(config.Configuration{})
defer os.Remove(c.ConfigPath())
c.SetOptions(config.OptionsConfiguration{
ProgressUpdateIntervalS: 60, // irrelevant, but must be positive
defer cfgCancel()
waiter, err := c.Modify(func(cfg *config.Configuration) {
cfg.Options.ProgressUpdateIntervalS = 60 // irrelevant, but must be positive
})
if err != nil {
t.Fatal(err)
}
waiter.Wait()
p := NewProgressEmitter(c, evLogger)
go p.Serve(ctx)
@ -109,12 +114,17 @@ func TestProgressEmitter(t *testing.T) {
}
func TestSendDownloadProgressMessages(t *testing.T) {
c := createTmpWrapper(config.Configuration{})
c, cfgCancel := createTmpWrapper(config.Configuration{})
defer os.Remove(c.ConfigPath())
c.SetOptions(config.OptionsConfiguration{
ProgressUpdateIntervalS: 60, // irrelevant, but must be positive
TempIndexMinBlocks: 10,
defer cfgCancel()
waiter, err := c.Modify(func(cfg *config.Configuration) {
cfg.Options.ProgressUpdateIntervalS = 60 // irrelevant, but must be positive
cfg.Options.TempIndexMinBlocks = 10
})
if err != nil {
t.Fatal(err)
}
waiter.Wait()
fc := &fakeConnection{}

View File

@ -29,7 +29,8 @@ func TestRequestSimple(t *testing.T) {
// Verify that the model performs a request and creates a file based on
// an incoming index update.
m, fc, fcfg := setupModelWithConnection(t)
m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
tfs := fcfg.Filesystem()
defer cleanupModelAndRemoveDir(m, tfs.URI())
@ -72,7 +73,8 @@ func TestSymlinkTraversalRead(t *testing.T) {
return
}
m, fc, fcfg := setupModelWithConnection(t)
m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
// We listen for incoming index updates and trigger when we see one for
@ -115,7 +117,8 @@ func TestSymlinkTraversalWrite(t *testing.T) {
return
}
m, fc, fcfg := setupModelWithConnection(t)
m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
// We listen for incoming index updates and trigger when we see one for
@ -174,7 +177,8 @@ func TestSymlinkTraversalWrite(t *testing.T) {
func TestRequestCreateTmpSymlink(t *testing.T) {
// Test that an update for a temporary file is invalidated
m, fc, fcfg := setupModelWithConnection(t)
m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
// We listen for incoming index updates and trigger when we see one for
@ -216,14 +220,15 @@ func TestRequestVersioningSymlinkAttack(t *testing.T) {
// Sets up a folder with trashcan versioning and tries to use a
// deleted symlink to escape
w, fcfg := tmpDefaultWrapper()
w, fcfg, wCancel := tmpDefaultWrapper()
defer wCancel()
defer func() {
os.RemoveAll(fcfg.Filesystem().URI())
os.Remove(w.ConfigPath())
}()
fcfg.Versioning = config.VersioningConfiguration{Type: "trashcan"}
w.SetFolder(fcfg)
setFolder(t, w, fcfg)
m, fc := setupModelWithConnectionFromWrapper(t, w)
defer cleanupModel(m)
@ -293,11 +298,12 @@ func TestPullInvalidIgnoredSR(t *testing.T) {
// This test checks that (un-)ignored/invalid/deleted files are treated as expected.
func pullInvalidIgnored(t *testing.T, ft config.FolderType) {
w := createTmpWrapper(defaultCfgWrapper.RawCopy())
w, wCancel := createTmpWrapper(defaultCfgWrapper.RawCopy())
defer wCancel()
fcfg := testFolderConfigTmp()
fss := fcfg.Filesystem()
fcfg.Type = ft
w.SetFolder(fcfg)
setFolder(t, w, fcfg)
m := setupModel(t, w)
defer cleanupModelAndRemoveDir(m, fss.URI())
@ -420,7 +426,8 @@ func pullInvalidIgnored(t *testing.T, ft config.FolderType) {
}
func TestIssue4841(t *testing.T) {
m, fc, fcfg := setupModelWithConnection(t)
m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
received := make(chan []protocol.FileInfo)
@ -464,7 +471,8 @@ func TestIssue4841(t *testing.T) {
}
func TestRescanIfHaveInvalidContent(t *testing.T) {
m, fc, fcfg := setupModelWithConnection(t)
m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
tfs := fcfg.Filesystem()
defer cleanupModelAndRemoveDir(m, tfs.URI())
@ -530,7 +538,8 @@ func TestRescanIfHaveInvalidContent(t *testing.T) {
}
func TestParentDeletion(t *testing.T) {
m, fc, fcfg := setupModelWithConnection(t)
m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
testFs := fcfg.Filesystem()
defer cleanupModelAndRemoveDir(m, testFs.URI())
@ -609,7 +618,8 @@ func TestRequestSymlinkWindows(t *testing.T) {
t.Skip("windows specific test")
}
m, fc, fcfg := setupModelWithConnection(t)
m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
received := make(chan []protocol.FileInfo)
@ -677,7 +687,8 @@ func equalContents(path string, contents []byte) error {
}
func TestRequestRemoteRenameChanged(t *testing.T) {
m, fc, fcfg := setupModelWithConnection(t)
m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
tfs := fcfg.Filesystem()
tmpDir := tfs.URI()
defer cleanupModelAndRemoveDir(m, tfs.URI())
@ -812,7 +823,8 @@ func TestRequestRemoteRenameChanged(t *testing.T) {
}
func TestRequestRemoteRenameConflict(t *testing.T) {
m, fc, fcfg := setupModelWithConnection(t)
m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
tfs := fcfg.Filesystem()
tmpDir := tfs.URI()
defer cleanupModelAndRemoveDir(m, tmpDir)
@ -903,7 +915,8 @@ func TestRequestRemoteRenameConflict(t *testing.T) {
}
func TestRequestDeleteChanged(t *testing.T) {
m, fc, fcfg := setupModelWithConnection(t)
m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
tfs := fcfg.Filesystem()
defer cleanupModelAndRemoveDir(m, tfs.URI())
@ -972,7 +985,8 @@ func TestRequestDeleteChanged(t *testing.T) {
}
func TestNeedFolderFiles(t *testing.T) {
m, fc, fcfg := setupModelWithConnection(t)
m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
tfs := fcfg.Filesystem()
tmpDir := tfs.URI()
defer cleanupModelAndRemoveDir(m, tmpDir)
@ -1020,7 +1034,8 @@ func TestNeedFolderFiles(t *testing.T) {
// propagated upon un-ignoring.
// https://github.com/syncthing/syncthing/issues/6038
func TestIgnoreDeleteUnignore(t *testing.T) {
w, fcfg := tmpDefaultWrapper()
w, fcfg, wCancel := tmpDefaultWrapper()
defer wCancel()
m := setupModel(t, w)
fss := fcfg.Filesystem()
tmpDir := fss.URI()
@ -1120,7 +1135,8 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
// TestRequestLastFileProgress checks that the last pulled file (here only) is registered
// as in progress.
func TestRequestLastFileProgress(t *testing.T) {
m, fc, fcfg := setupModelWithConnection(t)
m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
tfs := fcfg.Filesystem()
defer cleanupModelAndRemoveDir(m, tfs.URI())
@ -1156,7 +1172,8 @@ func TestRequestIndexSenderPause(t *testing.T) {
done := make(chan struct{})
defer close(done)
m, fc, fcfg := setupModelWithConnection(t)
m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
tfs := fcfg.Filesystem()
defer cleanupModelAndRemoveDir(m, tfs.URI())
@ -1215,13 +1232,8 @@ func TestRequestIndexSenderPause(t *testing.T) {
// Local paused and resume
fcfg.Paused = true
waiter, _ := m.cfg.SetFolder(fcfg)
waiter.Wait()
fcfg.Paused = false
waiter, _ = m.cfg.SetFolder(fcfg)
waiter.Wait()
pauseFolder(t, m.cfg, fcfg.ID, true)
pauseFolder(t, m.cfg, fcfg.ID, false)
seq++
files[0].Sequence = seq
@ -1238,16 +1250,12 @@ func TestRequestIndexSenderPause(t *testing.T) {
cc.Folders[0].Paused = true
m.ClusterConfig(device1, cc)
fcfg.Paused = true
waiter, _ = m.cfg.SetFolder(fcfg)
waiter.Wait()
pauseFolder(t, m.cfg, fcfg.ID, true)
cc.Folders[0].Paused = false
m.ClusterConfig(device1, cc)
fcfg.Paused = false
waiter, _ = m.cfg.SetFolder(fcfg)
waiter.Wait()
pauseFolder(t, m.cfg, fcfg.ID, false)
seq++
files[0].Sequence = seq
@ -1277,7 +1285,8 @@ func TestRequestIndexSenderPause(t *testing.T) {
}
func TestRequestIndexSenderClusterConfigBeforeStart(t *testing.T) {
w, fcfg := tmpDefaultWrapper()
w, fcfg, wCancel := tmpDefaultWrapper()
defer wCancel()
tfs := fcfg.Filesystem()
dir1 := "foo"
dir2 := "bar"
@ -1340,12 +1349,11 @@ func TestRequestIndexSenderClusterConfigBeforeStart(t *testing.T) {
}
func TestRequestReceiveEncryptedLocalNoSend(t *testing.T) {
w, fcfg := tmpDefaultWrapper()
w, fcfg, wCancel := tmpDefaultWrapper()
defer wCancel()
tfs := fcfg.Filesystem()
fcfg.Type = config.FolderTypeReceiveEncrypted
waiter, err := w.SetFolder(fcfg)
must(t, err)
waiter.Wait()
setFolder(t, w, fcfg)
encToken := protocol.PasswordToken(fcfg.ID, "pw")
must(t, tfs.Mkdir(config.DefaultMarkerName, 0777))

View File

@ -13,6 +13,8 @@ import (
"testing"
"time"
"github.com/thejerf/suture/v4"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/db/backend"
@ -24,12 +26,13 @@ import (
)
var (
myID, device1, device2 protocol.DeviceID
defaultCfgWrapper config.Wrapper
defaultFolderConfig config.FolderConfiguration
defaultFs fs.Filesystem
defaultCfg config.Configuration
defaultAutoAcceptCfg config.Configuration
myID, device1, device2 protocol.DeviceID
defaultCfgWrapper config.Wrapper
defaultCfgWrapperCancel context.CancelFunc
defaultFolderConfig config.FolderConfiguration
defaultFs fs.Filesystem
defaultCfg config.Configuration
defaultAutoAcceptCfg config.Configuration
)
func init() {
@ -40,12 +43,13 @@ func init() {
defaultFolderConfig = testFolderConfig("testdata")
defaultFs = defaultFolderConfig.Filesystem()
defaultCfgWrapper = createTmpWrapper(config.New(myID))
_, _ = defaultCfgWrapper.SetDevice(config.NewDeviceConfiguration(device1, "device1"))
_, _ = defaultCfgWrapper.SetFolder(defaultFolderConfig)
opts := defaultCfgWrapper.Options()
opts.KeepTemporariesH = 1
_, _ = defaultCfgWrapper.SetOptions(opts)
defaultCfgWrapper, defaultCfgWrapperCancel = createTmpWrapper(config.New(myID))
waiter, _ := defaultCfgWrapper.Modify(func(cfg *config.Configuration) {
cfg.SetDevice(config.NewDeviceConfiguration(device1, "device1"))
cfg.SetFolder(defaultFolderConfig)
cfg.Options.KeepTemporariesH = 1
})
waiter.Wait()
defaultCfg = defaultCfgWrapper.RawCopy()
@ -70,11 +74,28 @@ func init() {
}
}
func tmpDefaultWrapper() (config.Wrapper, config.FolderConfiguration) {
w := createTmpWrapper(defaultCfgWrapper.RawCopy())
func createTmpWrapper(cfg config.Configuration) (config.Wrapper, context.CancelFunc) {
tmpFile, err := ioutil.TempFile("", "syncthing-testConfig-")
if err != nil {
panic(err)
}
wrapper := config.Wrap(tmpFile.Name(), cfg, myID, events.NoopLogger)
tmpFile.Close()
if cfgService, ok := wrapper.(suture.Service); ok {
ctx, cancel := context.WithCancel(context.Background())
go cfgService.Serve(ctx)
return wrapper, cancel
}
return wrapper, func() {}
}
func tmpDefaultWrapper() (config.Wrapper, config.FolderConfiguration, context.CancelFunc) {
w, cancel := createTmpWrapper(defaultCfgWrapper.RawCopy())
fcfg := testFolderConfigTmp()
_, _ = w.SetFolder(fcfg)
return w, fcfg
_, _ = w.Modify(func(cfg *config.Configuration) {
cfg.SetFolder(fcfg)
})
return w, fcfg, cancel
}
func testFolderConfigTmp() config.FolderConfiguration {
@ -96,11 +117,11 @@ func testFolderConfigFake() config.FolderConfiguration {
return cfg
}
func setupModelWithConnection(t testing.TB) (*testModel, *fakeConnection, config.FolderConfiguration) {
func setupModelWithConnection(t testing.TB) (*testModel, *fakeConnection, config.FolderConfiguration, context.CancelFunc) {
t.Helper()
w, fcfg := tmpDefaultWrapper()
w, fcfg, cancel := tmpDefaultWrapper()
m, fc := setupModelWithConnectionFromWrapper(t, w)
return m, fc, fcfg
return m, fc, fcfg, cancel
}
func setupModelWithConnectionFromWrapper(t testing.TB, w config.Wrapper) (*testModel, *fakeConnection) {
@ -313,3 +334,70 @@ func newFileSet(t testing.TB, folder string, fs fs.Filesystem, ldb *db.Lowlevel)
}
return fset
}
func replace(t testing.TB, w config.Wrapper, to config.Configuration) {
t.Helper()
waiter, err := w.Modify(func(cfg *config.Configuration) {
*cfg = to
})
if err != nil {
t.Fatal(err)
}
waiter.Wait()
}
func pauseFolder(t testing.TB, w config.Wrapper, id string, paused bool) {
t.Helper()
waiter, err := w.Modify(func(cfg *config.Configuration) {
_, i, _ := cfg.Folder(id)
cfg.Folders[i].Paused = paused
})
if err != nil {
t.Fatal(err)
}
waiter.Wait()
}
func setFolder(t testing.TB, w config.Wrapper, fcfg config.FolderConfiguration) {
t.Helper()
waiter, err := w.Modify(func(cfg *config.Configuration) {
cfg.SetFolder(fcfg)
})
if err != nil {
t.Fatal(err)
}
waiter.Wait()
}
func pauseDevice(t testing.TB, w config.Wrapper, id protocol.DeviceID, paused bool) {
t.Helper()
waiter, err := w.Modify(func(cfg *config.Configuration) {
_, i, _ := cfg.Device(id)
cfg.Devices[i].Paused = paused
})
if err != nil {
t.Fatal(err)
}
waiter.Wait()
}
func setDevice(t testing.TB, w config.Wrapper, device config.DeviceConfiguration) {
t.Helper()
waiter, err := w.Modify(func(cfg *config.Configuration) {
cfg.SetDevice(device)
})
if err != nil {
t.Fatal(err)
}
waiter.Wait()
}
func addDevice2(t testing.TB, w config.Wrapper, fcfg config.FolderConfiguration) {
waiter, err := w.Modify(func(cfg *config.Configuration) {
cfg.SetDevice(config.NewDeviceConfiguration(device2, "device2"))
fcfg.Devices = append(fcfg.Devices, config.FolderDeviceConfiguration{DeviceID: device2})
cfg.SetFolder(fcfg)
})
must(t, err)
waiter.Wait()
}

View File

@ -121,6 +121,10 @@ func (a *App) Start() error {
}
func (a *App) startup() error {
if cfgService, ok := a.cfg.(suture.Service); ok {
a.mainService.Add(cfgService)
}
a.mainService.Add(ur.NewFailureHandler(a.cfg, a.evLogger))
a.mainService.Add(a.ll)
@ -277,24 +281,21 @@ func (a *App) startup() error {
a.mainService.Add(discoveryManager)
a.mainService.Add(connectionsService)
// Candidate builds always run with usage reporting.
if opts := a.cfg.Options(); build.IsCandidate {
l.Infoln("Anonymous usage reporting is always enabled for candidate releases.")
if opts.URAccepted != ur.Version {
opts.URAccepted = ur.Version
a.cfg.SetOptions(opts)
a.cfg.Save()
// Unique ID will be set and config saved below if necessary.
a.cfg.Modify(func(cfg *config.Configuration) {
// Candidate builds always run with usage reporting.
if build.IsCandidate {
l.Infoln("Anonymous usage reporting is always enabled for candidate releases.")
if cfg.Options.URAccepted != ur.Version {
cfg.Options.URAccepted = ur.Version
// Unique ID will be set and config saved below if necessary.
}
}
}
// If we are going to do usage reporting, ensure we have a valid unique ID.
if opts := a.cfg.Options(); opts.URAccepted > 0 && opts.URUniqueID == "" {
opts.URUniqueID = rand.String(8)
a.cfg.SetOptions(opts)
a.cfg.Save()
}
// If we are going to do usage reporting, ensure we have a valid unique ID.
if cfg.Options.URAccepted > 0 && cfg.Options.URUniqueID == "" {
cfg.Options.URUniqueID = rand.String(8)
}
})
usageReportingSvc := ur.New(a.cfg, m, connectionsService, a.opts.NoUpgrade)
a.mainService.Add(usageReportingSvc)

View File

@ -67,37 +67,17 @@ type failureStat struct {
}
func (h *failureHandler) Serve(ctx context.Context) error {
go func() {
select {
case h.optsChan <- h.cfg.Options():
case <-ctx.Done():
}
}()
h.cfg.Subscribe(h)
cfg := h.cfg.Subscribe(h)
defer h.cfg.Unsubscribe(h)
url, sub, evChan := h.applyOpts(cfg.Options, nil)
var url string
var err error
var sub events.Subscription
var evChan <-chan events.Event
timer := time.NewTimer(minDelay)
resetTimer := make(chan struct{})
outer:
for {
for err == nil {
select {
case opts := <-h.optsChan:
// Sub nil checks just for safety - config updates can be racy.
if opts.URAccepted > 0 {
if sub == nil {
sub = h.evLogger.Subscribe(events.Failure)
evChan = sub.C()
}
} else if sub != nil {
sub.Unsubscribe()
sub = nil
evChan = nil
}
url = opts.CRURL + "/failure"
url, sub, evChan = h.applyOpts(opts, sub)
case e, ok := <-evChan:
if !ok {
// Just to be safe - shouldn't ever happen, as
@ -137,7 +117,7 @@ outer:
case <-resetTimer:
timer.Reset(minDelay)
case <-ctx.Done():
break outer
err = ctx.Err()
}
}
@ -154,6 +134,21 @@ outer:
return err
}
func (h *failureHandler) applyOpts(opts config.OptionsConfiguration, sub events.Subscription) (string, events.Subscription, <-chan events.Event) {
// Sub nil checks just for safety - config updates can be racy.
url := opts.CRURL + "/failure"
if opts.URAccepted > 0 {
if sub == nil {
sub = h.evLogger.Subscribe(events.Failure)
}
return url, sub, sub.C()
}
if sub != nil {
sub.Unsubscribe()
}
return url, nil, nil
}
func (h *failureHandler) addReport(descr string, evTime time.Time) {
if stat, ok := h.buf[descr]; ok {
stat.last = evTime