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 := config.Wrap("config", config.New(id), id, events.NoopLogger)
wrapper.SetOptions(config.OptionsConfiguration{ wrapper.Modify(func(cfg *config.Configuration) {
NATLeaseM: natLease, cfg.Options.NATLeaseM = natLease
NATRenewalM: natRenewal, cfg.Options.NATRenewalM = natRenewal
NATTimeoutS: natTimeout, cfg.Options.NATTimeoutS = natTimeout
}) })
natSvc := nat.NewService(id, wrapper) natSvc := nat.NewService(id, wrapper)
mapping := mapping{natSvc.NewMapping(nat.TCP, addr.IP, addr.Port)} mapping := mapping{natSvc.NewMapping(nat.TCP, addr.IP, addr.Port)}

View File

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

View File

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

View File

@ -1253,6 +1253,11 @@ func TestConfigChanges(t *testing.T) {
defer os.Remove(tmpFile.Name()) defer os.Remove(tmpFile.Name())
w := config.Wrap(tmpFile.Name(), cfg, protocol.LocalDeviceID, events.NoopLogger) w := config.Wrap(tmpFile.Name(), cfg, protocol.LocalDeviceID, events.NoopLogger)
tmpFile.Close() 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) baseURL, cancel, err := startHTTP(w)
if err != nil { if err != nil {
t.Fatal("Unexpected error from getting base URL:", err) 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/config"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/sync"
) )
type configMuxBuilder struct { type configMuxBuilder struct {
*httprouter.Router *httprouter.Router
id protocol.DeviceID id protocol.DeviceID
cfg config.Wrapper cfg config.Wrapper
mut sync.Mutex
} }
func (c *configMuxBuilder) registerConfig(path string) { 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.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
c.mut.Lock()
defer c.mut.Unlock()
var folders []config.FolderConfiguration var folders []config.FolderConfiguration
if err := unmarshalTo(r.Body, &folders); err != nil { if err := unmarshalTo(r.Body, &folders); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
waiter, err := c.cfg.SetFolders(folders) waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
cfg.SetFolders(folders)
})
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -75,19 +73,7 @@ func (c *configMuxBuilder) registerFolders(path string) {
}) })
c.HandlerFunc(http.MethodPost, path, func(w http.ResponseWriter, r *http.Request) { c.HandlerFunc(http.MethodPost, path, func(w http.ResponseWriter, r *http.Request) {
c.mut.Lock() c.adjustFolder(w, r, config.FolderConfiguration{})
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)
}) })
} }
@ -97,14 +83,14 @@ func (c *configMuxBuilder) registerDevices(path string) {
}) })
c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) { c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
c.mut.Lock()
defer c.mut.Unlock()
var devices []config.DeviceConfiguration var devices []config.DeviceConfiguration
if err := unmarshalTo(r.Body, &devices); err != nil { if err := unmarshalTo(r.Body, &devices); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
waiter, err := c.cfg.SetDevices(devices) waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
cfg.SetDevices(devices)
})
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -113,14 +99,14 @@ func (c *configMuxBuilder) registerDevices(path string) {
}) })
c.HandlerFunc(http.MethodPost, path, func(w http.ResponseWriter, r *http.Request) { c.HandlerFunc(http.MethodPost, path, func(w http.ResponseWriter, r *http.Request) {
c.mut.Lock()
defer c.mut.Unlock()
var device config.DeviceConfiguration var device config.DeviceConfiguration
if err := unmarshalTo(r.Body, &device); err != nil { if err := unmarshalTo(r.Body, &device); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
waiter, err := c.cfg.SetDevice(device) waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
cfg.SetDevice(device)
})
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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.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")) waiter, err := c.cfg.RemoveFolder(p.ByName("id"))
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) 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.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")) id, err := protocol.DeviceIDFromString(p.ByName("id"))
waiter, err := c.cfg.RemoveDevice(id) waiter, err := c.cfg.RemoveDevice(id)
if err != nil { if err != nil {
@ -251,22 +233,27 @@ func (c *configMuxBuilder) registerGUI(path string) {
} }
func (c *configMuxBuilder) adjustConfig(w http.ResponseWriter, r *http.Request) { func (c *configMuxBuilder) adjustConfig(w http.ResponseWriter, r *http.Request) {
c.mut.Lock() to, err := config.ReadJSON(r.Body, c.id)
defer c.mut.Unlock()
cfg, err := config.ReadJSON(r.Body, c.id)
r.Body.Close() r.Body.Close()
if err != nil { if err != nil {
l.Warnln("Decoding posted config:", err) l.Warnln("Decoding posted config:", err)
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
if cfg.GUI.Password, err = checkGUIPassword(c.cfg.GUI().Password, cfg.GUI.Password); err != nil { var errMsg string
l.Warnln("bcrypting password:", err) var status int
http.Error(w, err.Error(), http.StatusInternalServerError) waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
return if to.GUI.Password, err = checkGUIPassword(cfg.GUI.Password, to.GUI.Password); err != nil {
} l.Warnln("bcrypting password:", err)
waiter, err := c.cfg.Replace(cfg) errMsg = err.Error()
if err != nil { status = http.StatusInternalServerError
return
}
*cfg = to
})
if errMsg != "" {
http.Error(w, errMsg, status)
} else if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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) { 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 { if err := unmarshalTo(r.Body, &folder); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
waiter, err := c.cfg.SetFolder(folder) waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
cfg.SetFolder(folder)
})
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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) { 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 { if err := unmarshalTo(r.Body, &device); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
waiter, err := c.cfg.SetDevice(device) waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
cfg.SetDevice(device)
})
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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) { 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 { if err := unmarshalTo(r.Body, &opts); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
waiter, err := c.cfg.SetOptions(opts) waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
cfg.Options = opts
})
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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) { func (c *configMuxBuilder) adjustGUI(w http.ResponseWriter, r *http.Request, gui config.GUIConfiguration) {
c.mut.Lock()
defer c.mut.Unlock()
oldPassword := gui.Password oldPassword := gui.Password
err := unmarshalTo(r.Body, &gui) err := unmarshalTo(r.Body, &gui)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
if gui.Password, err = checkGUIPassword(oldPassword, gui.Password); err != nil { var errMsg string
l.Warnln("bcrypting password:", err) var status int
http.Error(w, err.Error(), http.StatusInternalServerError) waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
return if gui.Password, err = checkGUIPassword(oldPassword, gui.Password); err != nil {
} l.Warnln("bcrypting password:", err)
waiter, err := c.cfg.SetGUI(gui) errMsg = err.Error()
if err != nil { 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) http.Error(w, err.Error(), http.StatusInternalServerError)
return 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) { 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 { if err := unmarshalTo(r.Body, &ldap); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
waiter, err := c.cfg.SetLDAP(ldap) waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
cfg.LDAP = ldap
})
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return

View File

@ -28,10 +28,6 @@ func (c *mockedConfig) LDAP() config.LDAPConfiguration {
return config.LDAPConfiguration{} return config.LDAPConfiguration{}
} }
func (c *mockedConfig) SetLDAP(config.LDAPConfiguration) (config.Waiter, error) {
return noopWaiter{}, nil
}
func (c *mockedConfig) RawCopy() config.Configuration { func (c *mockedConfig) RawCopy() config.Configuration {
cfg := config.Configuration{} cfg := config.Configuration{}
util.SetDefaults(&cfg.Options) util.SetDefaults(&cfg.Options)
@ -42,11 +38,13 @@ func (c *mockedConfig) Options() config.OptionsConfiguration {
return 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 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) {} func (c *mockedConfig) Unsubscribe(cm config.Committer) {}
@ -62,14 +60,6 @@ func (c *mockedConfig) DeviceList() []config.DeviceConfiguration {
return nil 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 { func (c *mockedConfig) Save() error {
return nil return nil
} }
@ -86,14 +76,6 @@ func (c *mockedConfig) ConfigPath() string {
return "" 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) { func (c *mockedConfig) Folder(id string) (config.FolderConfiguration, bool) {
return config.FolderConfiguration{}, false return config.FolderConfiguration{}, false
} }
@ -102,14 +84,6 @@ func (c *mockedConfig) FolderList() []config.FolderConfiguration {
return nil 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) { func (c *mockedConfig) RemoveFolder(id string) (config.Waiter, error) {
return noopWaiter{}, nil return noopWaiter{}, nil
} }

View File

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

View File

@ -372,6 +372,15 @@ func (cfg *Configuration) applyMigrations() {
migrationsMut.Unlock() 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. // DeviceMap returns a map of device ID to device configuration for the given configuration.
func (cfg *Configuration) DeviceMap() map[protocol.DeviceID]DeviceConfiguration { func (cfg *Configuration) DeviceMap() map[protocol.DeviceID]DeviceConfiguration {
m := make(map[protocol.DeviceID]DeviceConfiguration, len(cfg.Devices)) m := make(map[protocol.DeviceID]DeviceConfiguration, len(cfg.Devices))
@ -381,6 +390,44 @@ func (cfg *Configuration) DeviceMap() map[protocol.DeviceID]DeviceConfiguration
return m 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 // FolderPasswords returns the folder passwords set for this device, for
// folders that have an encryption password set. // folders that have an encryption password set.
func (cfg Configuration) FolderPasswords(device protocol.DeviceID) map[string]string { func (cfg Configuration) FolderPasswords(device protocol.DeviceID) map[string]string {
@ -397,6 +444,26 @@ nextFolder:
return res 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 { func ensureDevicePresent(devices []FolderDeviceConfiguration, myID protocol.DeviceID) []FolderDeviceConfiguration {
for _, device := range devices { for _, device := range devices {
if device.DeviceID.Equals(myID) { if device.DeviceID.Equals(myID) {

View File

@ -8,9 +8,11 @@ package config
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
@ -21,6 +23,7 @@ import (
"testing" "testing"
"github.com/d4l3k/messagediff" "github.com/d4l3k/messagediff"
"github.com/thejerf/suture/v4"
"github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/fs" "github.com/syncthing/syncthing/lib/fs"
@ -95,7 +98,8 @@ func TestDeviceConfig(t *testing.T) {
} }
os.RemoveAll(filepath.Join("testdata", DefaultMarkerName)) os.RemoveAll(filepath.Join("testdata", DefaultMarkerName))
wr, err := load(cfgFile, device1) wr, wrCancel, err := copyAndLoad(cfgFile, device1)
defer wrCancel()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -107,7 +111,7 @@ func TestDeviceConfig(t *testing.T) {
t.Fatal("Unexpected file") t.Fatal("Unexpected file")
} }
cfg := wr.(*wrapper).cfg cfg := wr.Wrapper.(*wrapper).cfg
expectedFolders := []FolderConfiguration{ expectedFolders := []FolderConfiguration{
{ {
@ -170,7 +174,8 @@ func TestDeviceConfig(t *testing.T) {
} }
func TestNoListenAddresses(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 { if err != nil {
t.Error(err) t.Error(err)
} }
@ -228,7 +233,8 @@ func TestOverriddenValues(t *testing.T) {
} }
os.Unsetenv("STNOUPGRADE") os.Unsetenv("STNOUPGRADE")
cfg, err := load("testdata/overridenvalues.xml", device1) cfg, cfgCancel, err := copyAndLoad("testdata/overridenvalues.xml", device1)
defer cfgCancel()
if err != nil { if err != nil {
t.Error(err) 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 { if err != nil {
t.Error(err) 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 { if err != nil {
t.Error(err) 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 { if err != nil {
t.Error(err) t.Error(err)
} }
@ -368,7 +377,8 @@ func TestDeviceAddressesStatic(t *testing.T) {
} }
func TestVersioningConfig(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 { if err != nil {
t.Error(err) 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") 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -409,7 +420,8 @@ func TestIssue1262(t *testing.T) {
} }
func TestIssue1750(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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -505,6 +517,7 @@ func TestFolderCheckPath(t *testing.T) {
func TestNewSaveLoad(t *testing.T) { func TestNewSaveLoad(t *testing.T) {
path := "testdata/temp.xml" path := "testdata/temp.xml"
os.Remove(path) os.Remove(path)
defer os.Remove(path)
exists := func(path string) bool { exists := func(path string) bool {
_, err := os.Stat(path) _, err := os.Stat(path)
@ -513,6 +526,7 @@ func TestNewSaveLoad(t *testing.T) {
intCfg := New(device1) intCfg := New(device1)
cfg := wrap(path, intCfg, device1) cfg := wrap(path, intCfg, device1)
defer cfg.stop()
if exists(path) { if exists(path) {
t.Error(path, "exists") t.Error(path, "exists")
@ -527,6 +541,7 @@ func TestNewSaveLoad(t *testing.T) {
} }
cfg2, err := load(path, device1) cfg2, err := load(path, device1)
defer cfg2.stop()
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -534,8 +549,6 @@ func TestNewSaveLoad(t *testing.T) {
if diff, equal := messagediff.PrettyDiff(cfg.RawCopy(), cfg2.RawCopy()); !equal { if diff, equal := messagediff.PrettyDiff(cfg.RawCopy(), cfg2.RawCopy()); !equal {
t.Errorf("Configs are not equal. Diff:\n%s", diff) t.Errorf("Configs are not equal. Diff:\n%s", diff)
} }
os.Remove(path)
} }
func TestPrepare(t *testing.T) { func TestPrepare(t *testing.T) {
@ -553,7 +566,8 @@ func TestPrepare(t *testing.T) {
} }
func TestCopy(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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -592,7 +606,8 @@ func TestCopy(t *testing.T) {
} }
func TestPullOrder(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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -632,8 +647,9 @@ func TestPullOrder(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
wrapper = wrap("testdata/pullorder.xml", cfg, device1) wrapper2 := wrap(wrapper.ConfigPath(), cfg, device1)
folders = wrapper.Folders() defer wrapper2.stop()
folders = wrapper2.Folders()
for _, tc := range expected { for _, tc := range expected {
if actual := folders[tc.name].Order; actual != tc.order { if actual := folders[tc.name].Order; actual != tc.order {
@ -643,7 +659,8 @@ func TestPullOrder(t *testing.T) {
} }
func TestLargeRescanInterval(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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -681,7 +698,8 @@ func TestGUIConfigURL(t *testing.T) {
func TestDuplicateDevices(t *testing.T) { func TestDuplicateDevices(t *testing.T) {
// Duplicate devices should be removed // 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -699,7 +717,8 @@ func TestDuplicateDevices(t *testing.T) {
func TestDuplicateFolders(t *testing.T) { func TestDuplicateFolders(t *testing.T) {
// Duplicate folders are a loading error // 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()) { if err == nil || !strings.Contains(err.Error(), errFolderIDDuplicate.Error()) {
t.Fatal(`Expected error to mention "duplicate folder ID":`, err) 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 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). // 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()) { if err == nil || !strings.Contains(err.Error(), errFolderPathEmpty.Error()) {
t.Fatal("Expected error due to empty folder path, got", err) 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 // Verify that ignored devices that are also present in the
// configuration are not in fact ignored. // 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -797,7 +818,8 @@ func TestIgnoredFolders(t *testing.T) {
// configuration are not in fact ignored. // configuration are not in fact ignored.
// Also, verify that folders that are shared with a device are not 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -833,7 +855,8 @@ func TestIgnoredFolders(t *testing.T) {
func TestGetDevice(t *testing.T) { func TestGetDevice(t *testing.T) {
// Verify that the Device() call does the right thing // 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -860,7 +883,8 @@ func TestGetDevice(t *testing.T) {
} }
func TestSharesRemovedOnDeviceRemoval(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 { if err != nil {
t.Errorf("Failed: %s", err) t.Errorf("Failed: %s", err)
} }
@ -872,10 +896,7 @@ func TestSharesRemovedOnDeviceRemoval(t *testing.T) {
t.Error("Should have less devices") t.Error("Should have less devices")
} }
_, err = wrapper.Replace(raw) replace(t, wrapper, raw)
if err != nil {
t.Errorf("Failed: %s", err)
}
raw = wrapper.RawCopy() raw = wrapper.RawCopy()
if len(raw.Folders[0].Devices) > len(raw.Devices) { if len(raw.Folders[0].Devices) > len(raw.Devices) {
@ -947,6 +968,7 @@ func TestIssue4219(t *testing.T) {
} }
w := wrap("/tmp/cfg", cfg, myID) w := wrap("/tmp/cfg", cfg, myID)
defer w.stop()
if !w.IgnoredFolder(device2, "t1") { if !w.IgnoredFolder(device2, "t1") {
t.Error("Folder device2 t1 should be ignored") t.Error("Folder device2 t1 should be ignored")
} }
@ -1157,13 +1179,80 @@ func defaultConfigAsMap() map[string]interface{} {
return tmp return tmp
} }
func load(path string, myID protocol.DeviceID) (Wrapper, error) { func copyToTmp(path string) (string, error) {
cfg, _, err := Load(path, myID, events.NoopLogger) orig, err := os.Open(path)
return cfg, err 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 { func copyAndLoad(path string, myID protocol.DeviceID) (*testWrapper, func(), error) {
return Wrap(path, cfg, myID, events.NoopLogger) 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) { func TestInternalVersioningConfiguration(t *testing.T) {

View File

@ -7,8 +7,12 @@
package config package config
import ( import (
"context"
"errors"
"os" "os"
"reflect"
"sync/atomic" "sync/atomic"
"time"
"github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/osutil" "github.com/syncthing/syncthing/lib/osutil"
@ -16,6 +20,13 @@ import (
"github.com/syncthing/syncthing/lib/sync" "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 // The Committer interface is implemented by objects that need to know about
// or have a say in configuration changes. // 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 // 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 // the new configuration will still have been applied by those who were
// capable of doing so. // 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 { type Committer interface {
VerifyConfiguration(from, to Configuration) error VerifyConfiguration(from, to Configuration) error
CommitConfiguration(from, to Configuration) (handled bool) CommitConfiguration(from, to Configuration) (handled bool)
@ -50,45 +65,47 @@ type noopWaiter struct{}
func (noopWaiter) Wait() {} func (noopWaiter) Wait() {}
// A Wrapper around a Configuration that manages loads, saves and published // ModifyFunction gets a pointer to a copy of the currently active configuration
// notifications of changes to registered Handlers // 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 { type Wrapper interface {
ConfigPath() string ConfigPath() string
MyID() protocol.DeviceID MyID() protocol.DeviceID
RawCopy() Configuration RawCopy() Configuration
Replace(cfg Configuration) (Waiter, error)
RequiresRestart() bool RequiresRestart() bool
Save() error Save() error
GUI() GUIConfiguration Modify(ModifyFunction) (Waiter, error)
SetGUI(gui GUIConfiguration) (Waiter, error) RemoveFolder(id string) (Waiter, error)
LDAP() LDAPConfiguration RemoveDevice(id protocol.DeviceID) (Waiter, error)
SetLDAP(ldap LDAPConfiguration) (Waiter, error)
GUI() GUIConfiguration
LDAP() LDAPConfiguration
Options() OptionsConfiguration Options() OptionsConfiguration
SetOptions(opts OptionsConfiguration) (Waiter, error)
Folder(id string) (FolderConfiguration, bool) Folder(id string) (FolderConfiguration, bool)
Folders() map[string]FolderConfiguration Folders() map[string]FolderConfiguration
FolderList() []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 FolderPasswords(device protocol.DeviceID) map[string]string
Device(id protocol.DeviceID) (DeviceConfiguration, bool) Device(id protocol.DeviceID) (DeviceConfiguration, bool)
Devices() map[protocol.DeviceID]DeviceConfiguration Devices() map[protocol.DeviceID]DeviceConfiguration
DeviceList() []DeviceConfiguration DeviceList() []DeviceConfiguration
RemoveDevice(id protocol.DeviceID) (Waiter, error)
SetDevice(DeviceConfiguration) (Waiter, error)
SetDevices([]DeviceConfiguration) (Waiter, error)
IgnoredDevices() []ObservedDevice IgnoredDevices() []ObservedDevice
IgnoredDevice(id protocol.DeviceID) bool IgnoredDevice(id protocol.DeviceID) bool
IgnoredFolder(device protocol.DeviceID, folder string) bool IgnoredFolder(device protocol.DeviceID, folder string) bool
Subscribe(c Committer) Subscribe(c Committer) Configuration
Unsubscribe(c Committer) Unsubscribe(c Committer)
} }
@ -97,6 +114,7 @@ type wrapper struct {
path string path string
evLogger events.Logger evLogger events.Logger
myID protocol.DeviceID myID protocol.DeviceID
queue chan modifyEntry
waiter Waiter // Latest ongoing config change waiter Waiter // Latest ongoing config change
subs []Committer subs []Committer
@ -107,12 +125,15 @@ type wrapper struct {
// Wrap wraps an existing Configuration structure and ties it to a file on // Wrap wraps an existing Configuration structure and ties it to a file on
// disk. // 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 { func Wrap(path string, cfg Configuration, myID protocol.DeviceID, evLogger events.Logger) Wrapper {
w := &wrapper{ w := &wrapper{
cfg: cfg, cfg: cfg,
path: path, path: path,
evLogger: evLogger, evLogger: evLogger,
myID: myID, myID: myID,
queue: make(chan modifyEntry, maxModifications),
waiter: noopWaiter{}, // Noop until first config change waiter: noopWaiter{}, // Noop until first config change
mut: sync.NewMutex(), 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 // Load loads an existing file on disk and returns a new configuration
// wrapper. // 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) { func Load(path string, myID protocol.DeviceID, evLogger events.Logger) (Wrapper, int, error) {
fd, err := os.Open(path) fd, err := os.Open(path)
if err != nil { if err != nil {
@ -145,11 +168,13 @@ func (w *wrapper) MyID() protocol.DeviceID {
} }
// Subscribe registers the given handler to be called on any future // Subscribe registers the given handler to be called on any future
// configuration changes. // configuration changes. It returns the config that is in effect while
func (w *wrapper) Subscribe(c Committer) { // subscribing, that can be used for initial setup.
func (w *wrapper) Subscribe(c Committer) Configuration {
w.mut.Lock() w.mut.Lock()
defer w.mut.Unlock()
w.subs = append(w.subs, c) w.subs = append(w.subs, c)
w.mut.Unlock() return w.cfg.Copy()
} }
// Unsubscribe de-registers the given handler from any future calls to // Unsubscribe de-registers the given handler from any future calls to
@ -179,11 +204,84 @@ func (w *wrapper) RawCopy() Configuration {
return w.cfg.Copy() return w.cfg.Copy()
} }
// Replace swaps the current configuration object for the given one. func (w *wrapper) Modify(fn ModifyFunction) (Waiter, error) {
func (w *wrapper) Replace(cfg Configuration) (Waiter, error) { return w.modifyQueued(fn)
w.mut.Lock() }
defer w.mut.Unlock()
return w.replaceLocked(cfg.Copy()) 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) { func (w *wrapper) replaceLocked(to Configuration) (Waiter, error) {
@ -246,55 +344,16 @@ func (w *wrapper) DeviceList() []DeviceConfiguration {
return w.cfg.Copy().Devices 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 // RemoveDevice removes the device from the configuration
func (w *wrapper) RemoveDevice(id protocol.DeviceID) (Waiter, error) { func (w *wrapper) RemoveDevice(id protocol.DeviceID) (Waiter, error) {
w.mut.Lock() return w.modifyQueued(func(cfg *Configuration) {
defer w.mut.Unlock() if _, i, ok := cfg.Device(id); ok {
cfg.Devices = append(cfg.Devices[:i], cfg.Devices[i+1:]...)
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 noopWaiter{}, nil
} }
// Folders returns a map of folders. Folder structures should not be changed, // Folders returns a map of folders.
// other than for the purpose of updating via SetFolder().
func (w *wrapper) Folders() map[string]FolderConfiguration { func (w *wrapper) Folders() map[string]FolderConfiguration {
w.mut.Lock() w.mut.Lock()
defer w.mut.Unlock() defer w.mut.Unlock()
@ -312,51 +371,13 @@ func (w *wrapper) FolderList() []FolderConfiguration {
return w.cfg.Copy().Folders 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 // RemoveFolder removes the folder from the configuration
func (w *wrapper) RemoveFolder(id string) (Waiter, error) { func (w *wrapper) RemoveFolder(id string) (Waiter, error) {
w.mut.Lock() return w.modifyQueued(func(cfg *Configuration) {
defer w.mut.Unlock() if _, i, ok := cfg.Folder(id); ok {
cfg.Folders = append(cfg.Folders[:i], cfg.Folders[i+1:]...)
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 noopWaiter{}, nil
} }
// FolderPasswords returns the folder passwords set for this device, for // FolderPasswords returns the folder passwords set for this device, for
@ -374,29 +395,12 @@ func (w *wrapper) Options() OptionsConfiguration {
return w.cfg.Options.Copy() 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 { func (w *wrapper) LDAP() LDAPConfiguration {
w.mut.Lock() w.mut.Lock()
defer w.mut.Unlock() defer w.mut.Unlock()
return w.cfg.LDAP.Copy() 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. // GUI returns the current GUI configuration object.
func (w *wrapper) GUI() GUIConfiguration { func (w *wrapper) GUI() GUIConfiguration {
w.mut.Lock() w.mut.Lock()
@ -404,15 +408,6 @@ func (w *wrapper) GUI() GUIConfiguration {
return w.cfg.GUI.Copy() 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 // IgnoredDevice returns whether or not connection attempts from the given
// device should be silently ignored. // device should be silently ignored.
func (w *wrapper) IgnoredDevice(id protocol.DeviceID) bool { 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) { func (w *wrapper) Device(id protocol.DeviceID) (DeviceConfiguration, bool) {
w.mut.Lock() w.mut.Lock()
defer w.mut.Unlock() defer w.mut.Unlock()
for _, device := range w.cfg.Devices { device, _, ok := w.cfg.Device(id)
if device.DeviceID == id { if !ok {
return device.Copy(), true return DeviceConfiguration{}, false
}
} }
return DeviceConfiguration{}, false return device.Copy(), ok
} }
// Folder returns the configuration for the given folder and an "ok" bool. // Folder returns the configuration for the given folder and an "ok" bool.
func (w *wrapper) Folder(id string) (FolderConfiguration, bool) { func (w *wrapper) Folder(id string) (FolderConfiguration, bool) {
w.mut.Lock() w.mut.Lock()
defer w.mut.Unlock() defer w.mut.Unlock()
for _, folder := range w.cfg.Folders { fcfg, _, ok := w.cfg.Folder(id)
if folder.ID == id { if !ok {
return folder.Copy(), true return FolderConfiguration{}, false
}
} }
return FolderConfiguration{}, false return fcfg.Copy(), ok
} }
// Save writes the configuration to disk, and generates a ConfigSaved event. // Save writes the configuration to disk, and generates a ConfigSaved event.
@ -502,3 +495,13 @@ func (w *wrapper) RequiresRestart() bool {
func (w *wrapper) setRequiresRestart() { func (w *wrapper) setRequiresRestart() {
atomic.StoreUint32(&w.requiresRestart, 1) 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 ( import (
"bytes" "bytes"
"context"
crand "crypto/rand" crand "crypto/rand"
"io" "io"
"math/rand" "math/rand"
@ -16,6 +17,7 @@ import (
"github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
"github.com/thejerf/suture/v4"
"golang.org/x/time/rate" "golang.org/x/time/rate"
) )
@ -29,24 +31,34 @@ func init() {
device4, _ = protocol.DeviceIDFromString("P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2") device4, _ = protocol.DeviceIDFromString("P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2")
} }
func initConfig() config.Wrapper { func initConfig() (config.Wrapper, context.CancelFunc) {
cfg := config.Wrap("/dev/null", config.New(device1), device1, events.NoopLogger) wrapper := config.Wrap("/dev/null", config.New(device1), device1, events.NoopLogger)
dev1Conf = config.NewDeviceConfiguration(device1, "device1") dev1Conf = config.NewDeviceConfiguration(device1, "device1")
dev2Conf = config.NewDeviceConfiguration(device2, "device2") dev2Conf = config.NewDeviceConfiguration(device2, "device2")
dev3Conf = config.NewDeviceConfiguration(device3, "device3") dev3Conf = config.NewDeviceConfiguration(device3, "device3")
dev4Conf = config.NewDeviceConfiguration(device4, "device4") 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.MaxRecvKbps = rand.Int() % 100000
dev2Conf.MaxSendKbps = 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() waiter.Wait()
return cfg return wrapper, cancel
} }
func TestLimiterInit(t *testing.T) { func TestLimiterInit(t *testing.T) {
cfg := initConfig() wrapper, wrapperCancel := initConfig()
lim := newLimiter(device1, cfg) defer wrapperCancel()
lim := newLimiter(device1, wrapper)
device2ReadLimit := dev2Conf.MaxRecvKbps device2ReadLimit := dev2Conf.MaxRecvKbps
device2WriteLimit := dev2Conf.MaxSendKbps device2WriteLimit := dev2Conf.MaxSendKbps
@ -70,8 +82,9 @@ func TestLimiterInit(t *testing.T) {
} }
func TestSetDeviceLimits(t *testing.T) { func TestSetDeviceLimits(t *testing.T) {
cfg := initConfig() wrapper, wrapperCancel := initConfig()
lim := newLimiter(device1, cfg) defer wrapperCancel()
lim := newLimiter(device1, wrapper)
// should still be inf/inf because this is local device // should still be inf/inf because this is local device
dev1ReadLimit := rand.Int() % 100000 dev1ReadLimit := rand.Int() % 100000
@ -87,7 +100,9 @@ func TestSetDeviceLimits(t *testing.T) {
dev3ReadLimit := rand.Int() % 10000 dev3ReadLimit := rand.Int() % 10000
dev3Conf.MaxRecvKbps = dev3ReadLimit 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() waiter.Wait()
expectedR := map[protocol.DeviceID]*rate.Limiter{ expectedR := map[protocol.DeviceID]*rate.Limiter{
@ -108,10 +123,11 @@ func TestSetDeviceLimits(t *testing.T) {
} }
func TestRemoveDevice(t *testing.T) { func TestRemoveDevice(t *testing.T) {
cfg := initConfig() wrapper, wrapperCancel := initConfig()
lim := newLimiter(device1, cfg) defer wrapperCancel()
lim := newLimiter(device1, wrapper)
waiter, _ := cfg.RemoveDevice(device3) waiter, _ := wrapper.RemoveDevice(device3)
waiter.Wait() waiter.Wait()
expectedR := map[protocol.DeviceID]*rate.Limiter{ expectedR := map[protocol.DeviceID]*rate.Limiter{
device2: rate.NewLimiter(rate.Limit(dev2Conf.MaxRecvKbps*1024), limiterBurstSize), device2: rate.NewLimiter(rate.Limit(dev2Conf.MaxRecvKbps*1024), limiterBurstSize),
@ -128,15 +144,18 @@ func TestRemoveDevice(t *testing.T) {
} }
func TestAddDevice(t *testing.T) { func TestAddDevice(t *testing.T) {
cfg := initConfig() wrapper, wrapperCancel := initConfig()
lim := newLimiter(device1, cfg) defer wrapperCancel()
lim := newLimiter(device1, wrapper)
addedDevice, _ := protocol.DeviceIDFromString("XZJ4UNS-ENI7QGJ-J45DT6G-QSGML2K-6I4XVOG-NAZ7BF5-2VAOWNT-TFDOMQU") addedDevice, _ := protocol.DeviceIDFromString("XZJ4UNS-ENI7QGJ-J45DT6G-QSGML2K-6I4XVOG-NAZ7BF5-2VAOWNT-TFDOMQU")
addDevConf := config.NewDeviceConfiguration(addedDevice, "addedDevice") addDevConf := config.NewDeviceConfiguration(addedDevice, "addedDevice")
addDevConf.MaxRecvKbps = 120 addDevConf.MaxRecvKbps = 120
addDevConf.MaxSendKbps = 240 addDevConf.MaxSendKbps = 240
waiter, _ := cfg.SetDevice(addDevConf) waiter, _ := wrapper.Modify(func(cfg *config.Configuration) {
cfg.SetDevice(addDevConf)
})
waiter.Wait() waiter.Wait()
expectedR := map[protocol.DeviceID]*rate.Limiter{ expectedR := map[protocol.DeviceID]*rate.Limiter{
@ -159,17 +178,20 @@ func TestAddDevice(t *testing.T) {
} }
func TestAddAndRemove(t *testing.T) { func TestAddAndRemove(t *testing.T) {
cfg := initConfig() wrapper, wrapperCancel := initConfig()
lim := newLimiter(device1, cfg) defer wrapperCancel()
lim := newLimiter(device1, wrapper)
addedDevice, _ := protocol.DeviceIDFromString("XZJ4UNS-ENI7QGJ-J45DT6G-QSGML2K-6I4XVOG-NAZ7BF5-2VAOWNT-TFDOMQU") addedDevice, _ := protocol.DeviceIDFromString("XZJ4UNS-ENI7QGJ-J45DT6G-QSGML2K-6I4XVOG-NAZ7BF5-2VAOWNT-TFDOMQU")
addDevConf := config.NewDeviceConfiguration(addedDevice, "addedDevice") addDevConf := config.NewDeviceConfiguration(addedDevice, "addedDevice")
addDevConf.MaxRecvKbps = 120 addDevConf.MaxRecvKbps = 120
addDevConf.MaxSendKbps = 240 addDevConf.MaxSendKbps = 240
waiter, _ := cfg.SetDevice(addDevConf) waiter, _ := wrapper.Modify(func(cfg *config.Configuration) {
cfg.SetDevice(addDevConf)
})
waiter.Wait() waiter.Wait()
waiter, _ = cfg.RemoveDevice(device3) waiter, _ = wrapper.RemoveDevice(device3)
waiter.Wait() waiter.Wait()
expectedR := map[protocol.DeviceID]*rate.Limiter{ expectedR := map[protocol.DeviceID]*rate.Limiter{

View File

@ -25,7 +25,8 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
// Get us a model up and running // Get us a model up and running
m, f := setupROFolder(t) m, f, wcfgCancel := setupROFolder(t)
defer wcfgCancel()
ffs := f.Filesystem() ffs := f.Filesystem()
defer cleanupModel(m) defer cleanupModel(m)
@ -105,7 +106,8 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
// Get us a model up and running // Get us a model up and running
m, f := setupROFolder(t) m, f, wcfgCancel := setupROFolder(t)
defer wcfgCancel()
ffs := f.Filesystem() ffs := f.Filesystem()
defer cleanupModel(m) defer cleanupModel(m)
@ -193,7 +195,8 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
func TestRecvOnlyUndoChanges(t *testing.T) { func TestRecvOnlyUndoChanges(t *testing.T) {
// Get us a model up and running // Get us a model up and running
m, f := setupROFolder(t) m, f, wcfgCancel := setupROFolder(t)
defer wcfgCancel()
ffs := f.Filesystem() ffs := f.Filesystem()
defer cleanupModel(m) defer cleanupModel(m)
@ -261,7 +264,8 @@ func TestRecvOnlyUndoChanges(t *testing.T) {
func TestRecvOnlyDeletedRemoteDrop(t *testing.T) { func TestRecvOnlyDeletedRemoteDrop(t *testing.T) {
// Get us a model up and running // Get us a model up and running
m, f := setupROFolder(t) m, f, wcfgCancel := setupROFolder(t)
defer wcfgCancel()
ffs := f.Filesystem() ffs := f.Filesystem()
defer cleanupModel(m) defer cleanupModel(m)
@ -324,7 +328,8 @@ func TestRecvOnlyDeletedRemoteDrop(t *testing.T) {
func TestRecvOnlyRemoteUndoChanges(t *testing.T) { func TestRecvOnlyRemoteUndoChanges(t *testing.T) {
// Get us a model up and running // Get us a model up and running
m, f := setupROFolder(t) m, f, wcfgCancel := setupROFolder(t)
defer wcfgCancel()
ffs := f.Filesystem() ffs := f.Filesystem()
defer cleanupModel(m) defer cleanupModel(m)
@ -443,17 +448,17 @@ func setupKnownFiles(t *testing.T, ffs fs.Filesystem, data []byte) []protocol.Fi
return knownFiles return knownFiles
} }
func setupROFolder(t *testing.T) (*testModel, *receiveOnlyFolder) { func setupROFolder(t *testing.T) (*testModel, *receiveOnlyFolder, context.CancelFunc) {
t.Helper() t.Helper()
w := createTmpWrapper(defaultCfg) w, cancel := createTmpWrapper(defaultCfg)
cfg := w.RawCopy() cfg := w.RawCopy()
fcfg := testFolderConfigFake() fcfg := testFolderConfigFake()
fcfg.ID = "ro" fcfg.ID = "ro"
fcfg.Label = "ro" fcfg.Label = "ro"
fcfg.Type = config.FolderTypeReceiveOnly fcfg.Type = config.FolderTypeReceiveOnly
cfg.Folders = []config.FolderConfiguration{fcfg} cfg.Folders = []config.FolderConfiguration{fcfg}
w.Replace(cfg) replace(t, w, cfg)
m := newModel(t, w, myID, "syncthing", "dev", nil) m := newModel(t, w, myID, "syncthing", "dev", nil)
m.ServeBackground() m.ServeBackground()
@ -464,7 +469,7 @@ func setupROFolder(t *testing.T) (*testModel, *receiveOnlyFolder) {
defer m.fmut.RUnlock() defer m.fmut.RUnlock()
f := m.folderRunners["ro"].(*receiveOnlyFolder) 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 { 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. // 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) { func setupSendReceiveFolder(t testing.TB, files ...protocol.FileInfo) (*testModel, *sendReceiveFolder, context.CancelFunc) {
w, fcfg := tmpDefaultWrapper() w, fcfg, wCancel := tmpDefaultWrapper()
// Initialise model and stop immediately. // Initialise model and stop immediately.
model := setupModel(t, w) model := setupModel(t, w)
model.cancel() model.cancel()
@ -107,10 +107,11 @@ func setupSendReceiveFolder(t testing.TB, files ...protocol.FileInfo) (*testMode
f.updateLocalsFromScanning(files) 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.Remove(m.cfg.ConfigPath())
os.RemoveAll(f.Filesystem().URI()) os.RemoveAll(f.Filesystem().URI())
} }
@ -130,8 +131,8 @@ func TestHandleFile(t *testing.T) {
requiredFile := existingFile requiredFile := existingFile
requiredFile.Blocks = blocks[1:] requiredFile.Blocks = blocks[1:]
m, f := setupSendReceiveFolder(t, existingFile) m, f, wcfgCancel := setupSendReceiveFolder(t, existingFile)
defer cleanupSRFolder(f, m) defer cleanupSRFolder(f, m, wcfgCancel)
copyChan := make(chan copyBlocksState, 1) copyChan := make(chan copyBlocksState, 1)
@ -172,8 +173,8 @@ func TestHandleFileWithTemp(t *testing.T) {
requiredFile := existingFile requiredFile := existingFile
requiredFile.Blocks = blocks[1:] requiredFile.Blocks = blocks[1:]
m, f := setupSendReceiveFolder(t, existingFile) m, f, wcfgCancel := setupSendReceiveFolder(t, existingFile)
defer cleanupSRFolder(f, m) defer cleanupSRFolder(f, m, wcfgCancel)
if _, err := prepareTmpFile(f.Filesystem()); err != nil { if _, err := prepareTmpFile(f.Filesystem()); err != nil {
t.Fatal(err) t.Fatal(err)
@ -228,10 +229,10 @@ func TestCopierFinder(t *testing.T) {
requiredFile.Blocks = blocks[1:] requiredFile.Blocks = blocks[1:]
requiredFile.Name = "file2" requiredFile.Name = "file2"
m, f := setupSendReceiveFolder(t, existingFile) m, f, wcfgCancel := setupSendReceiveFolder(t, existingFile)
f.CopyRangeMethod = method f.CopyRangeMethod = method
defer cleanupSRFolder(f, m) defer cleanupSRFolder(f, m, wcfgCancel)
if _, err := prepareTmpFile(f.Filesystem()); err != nil { if _, err := prepareTmpFile(f.Filesystem()); err != nil {
t.Fatal(err) t.Fatal(err)
@ -309,8 +310,8 @@ func TestCopierFinder(t *testing.T) {
func TestWeakHash(t *testing.T) { func TestWeakHash(t *testing.T) {
// Setup the model/pull environment // Setup the model/pull environment
model, fo := setupSendReceiveFolder(t) model, fo, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(fo, model) defer cleanupSRFolder(fo, model, wcfgCancel)
ffs := fo.Filesystem() ffs := fo.Filesystem()
tempFile := fs.TempName("weakhash") tempFile := fs.TempName("weakhash")
@ -438,8 +439,8 @@ func TestCopierCleanup(t *testing.T) {
// Create a file // Create a file
file := setupFile("test", []int{0}) file := setupFile("test", []int{0})
file.Size = 1 file.Size = 1
m, f := setupSendReceiveFolder(t, file) m, f, wcfgCancel := setupSendReceiveFolder(t, file)
defer cleanupSRFolder(f, m) defer cleanupSRFolder(f, m, wcfgCancel)
file.Blocks = []protocol.BlockInfo{blocks[1]} file.Blocks = []protocol.BlockInfo{blocks[1]}
file.Version = file.Version.Update(myID.Short()) file.Version = file.Version.Update(myID.Short())
@ -471,8 +472,8 @@ func TestCopierCleanup(t *testing.T) {
func TestDeregisterOnFailInCopy(t *testing.T) { func TestDeregisterOnFailInCopy(t *testing.T) {
file := setupFile("filex", []int{0, 2, 0, 0, 5, 0, 0, 8}) file := setupFile("filex", []int{0, 2, 0, 0, 5, 0, 0, 8})
m, f := setupSendReceiveFolder(t) m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m) defer cleanupSRFolder(f, m, wcfgCancel)
// Set up our evet subscription early // Set up our evet subscription early
s := m.evLogger.Subscribe(events.ItemFinished) s := m.evLogger.Subscribe(events.ItemFinished)
@ -571,8 +572,8 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
func TestDeregisterOnFailInPull(t *testing.T) { func TestDeregisterOnFailInPull(t *testing.T) {
file := setupFile("filex", []int{0, 2, 0, 0, 5, 0, 0, 8}) file := setupFile("filex", []int{0, 2, 0, 0, 5, 0, 0, 8})
m, f := setupSendReceiveFolder(t) m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m) defer cleanupSRFolder(f, m, wcfgCancel)
// Set up our evet subscription early // Set up our evet subscription early
s := m.evLogger.Subscribe(events.ItemFinished) s := m.evLogger.Subscribe(events.ItemFinished)
@ -674,8 +675,8 @@ func TestDeregisterOnFailInPull(t *testing.T) {
} }
func TestIssue3164(t *testing.T) { func TestIssue3164(t *testing.T) {
m, f := setupSendReceiveFolder(t) m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m) defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem() ffs := f.Filesystem()
tmpDir := ffs.URI() 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 // option is true and the permissions do not match between the file on disk and
// in the db. // in the db.
func TestDeleteIgnorePerms(t *testing.T) { func TestDeleteIgnorePerms(t *testing.T) {
m, f := setupSendReceiveFolder(t) m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m) defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem() ffs := f.Filesystem()
f.IgnorePerms = true 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 // Set up a folder with the CopyParentOwner bit and backed by a fake
// filesystem. // filesystem.
m, f := setupSendReceiveFolder(t) m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m) defer cleanupSRFolder(f, m, wcfgCancel)
f.folder.FolderConfiguration = config.NewFolderConfiguration(m.id, f.ID, f.Label, fs.FilesystemTypeFake, "/TestCopyOwner") f.folder.FolderConfiguration = config.NewFolderConfiguration(m.id, f.ID, f.Label, fs.FilesystemTypeFake, "/TestCopyOwner")
f.folder.FolderConfiguration.CopyOwnershipFromParent = true 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 // TestSRConflictReplaceFileByDir checks that a conflict is created when an existing file
// is replaced with a directory and versions are conflicting // is replaced with a directory and versions are conflicting
func TestSRConflictReplaceFileByDir(t *testing.T) { func TestSRConflictReplaceFileByDir(t *testing.T) {
m, f := setupSendReceiveFolder(t) m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m) defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem() ffs := f.Filesystem()
name := "foo" name := "foo"
@ -938,8 +939,8 @@ func TestSRConflictReplaceFileByDir(t *testing.T) {
// TestSRConflictReplaceFileByLink checks that a conflict is created when an existing file // TestSRConflictReplaceFileByLink checks that a conflict is created when an existing file
// is replaced with a link and versions are conflicting // is replaced with a link and versions are conflicting
func TestSRConflictReplaceFileByLink(t *testing.T) { func TestSRConflictReplaceFileByLink(t *testing.T) {
m, f := setupSendReceiveFolder(t) m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m) defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem() ffs := f.Filesystem()
name := "foo" name := "foo"
@ -971,8 +972,8 @@ func TestSRConflictReplaceFileByLink(t *testing.T) {
// TestDeleteBehindSymlink checks that we don't delete or schedule a scan // TestDeleteBehindSymlink checks that we don't delete or schedule a scan
// when trying to delete a file behind a symlink. // when trying to delete a file behind a symlink.
func TestDeleteBehindSymlink(t *testing.T) { func TestDeleteBehindSymlink(t *testing.T) {
m, f := setupSendReceiveFolder(t) m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m) defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem() ffs := f.Filesystem()
destDir := createTmpDir() destDir := createTmpDir()
@ -1022,8 +1023,8 @@ func TestDeleteBehindSymlink(t *testing.T) {
// Reproduces https://github.com/syncthing/syncthing/issues/6559 // Reproduces https://github.com/syncthing/syncthing/issues/6559
func TestPullCtxCancel(t *testing.T) { func TestPullCtxCancel(t *testing.T) {
m, f := setupSendReceiveFolder(t) m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m) defer cleanupSRFolder(f, m, wcfgCancel)
pullChan := make(chan pullBlockState) pullChan := make(chan pullBlockState)
finisherChan := make(chan *sharedPullerState) finisherChan := make(chan *sharedPullerState)
@ -1064,8 +1065,8 @@ func TestPullCtxCancel(t *testing.T) {
} }
func TestPullDeleteUnscannedDir(t *testing.T) { func TestPullDeleteUnscannedDir(t *testing.T) {
m, f := setupSendReceiveFolder(t) m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m) defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem() ffs := f.Filesystem()
dir := "foobar" dir := "foobar"
@ -1093,8 +1094,8 @@ func TestPullDeleteUnscannedDir(t *testing.T) {
} }
func TestPullCaseOnlyPerformFinish(t *testing.T) { func TestPullCaseOnlyPerformFinish(t *testing.T) {
m, f := setupSendReceiveFolder(t) m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m) defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem() ffs := f.Filesystem()
name := "foo" name := "foo"
@ -1154,8 +1155,8 @@ func TestPullCaseOnlySymlink(t *testing.T) {
} }
func testPullCaseOnlyDirOrSymlink(t *testing.T, dir bool) { func testPullCaseOnlyDirOrSymlink(t *testing.T, dir bool) {
m, f := setupSendReceiveFolder(t) m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m) defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem() ffs := f.Filesystem()
name := "foo" name := "foo"
@ -1209,8 +1210,8 @@ func testPullCaseOnlyDirOrSymlink(t *testing.T, dir bool) {
} }
func TestPullTempFileCaseConflict(t *testing.T) { func TestPullTempFileCaseConflict(t *testing.T) {
m, f := setupSendReceiveFolder(t) m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m) defer cleanupSRFolder(f, m, wcfgCancel)
copyChan := make(chan copyBlocksState, 1) copyChan := make(chan copyBlocksState, 1)
@ -1235,8 +1236,8 @@ func TestPullTempFileCaseConflict(t *testing.T) {
} }
func TestPullCaseOnlyRename(t *testing.T) { func TestPullCaseOnlyRename(t *testing.T) {
m, f := setupSendReceiveFolder(t) m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m) defer cleanupSRFolder(f, m, wcfgCancel)
// tempNameConfl := fs.TempName(confl) // tempNameConfl := fs.TempName(confl)
@ -1278,8 +1279,8 @@ func TestPullSymlinkOverExistingWindows(t *testing.T) {
t.Skip() t.Skip()
} }
m, f := setupSendReceiveFolder(t) m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m) defer cleanupSRFolder(f, m, wcfgCancel)
name := "foo" name := "foo"
if fd, err := f.mtimefs.Create(name); err != nil { if fd, err := f.mtimefs.Create(name); err != nil {
@ -1318,8 +1319,8 @@ func TestPullSymlinkOverExistingWindows(t *testing.T) {
} }
func TestPullDeleteCaseConflict(t *testing.T) { func TestPullDeleteCaseConflict(t *testing.T) {
m, f := setupSendReceiveFolder(t) m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m) defer cleanupSRFolder(f, m, wcfgCancel)
name := "foo" name := "foo"
fi := protocol.FileInfo{Name: "Foo"} fi := protocol.FileInfo{Name: "Foo"}
@ -1352,8 +1353,8 @@ func TestPullDeleteCaseConflict(t *testing.T) {
} }
func TestPullDeleteIgnoreChildDir(t *testing.T) { func TestPullDeleteIgnoreChildDir(t *testing.T) {
m, f := setupSendReceiveFolder(t) m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m) defer cleanupSRFolder(f, m, wcfgCancel)
parent := "parent" parent := "parent"
del := "ignored" del := "ignored"

View File

@ -136,6 +136,7 @@ type model struct {
// such as scans and pulls. // such as scans and pulls.
folderIOLimiter *byteSemaphore folderIOLimiter *byteSemaphore
fatalChan chan error fatalChan chan error
started chan struct{}
// fields protected by fmut // fields protected by fmut
fmut sync.RWMutex fmut sync.RWMutex
@ -162,7 +163,6 @@ type model struct {
// for testing only // for testing only
foldersRunning int32 foldersRunning int32
started chan struct{}
} }
type folderFactory func(*model, *db.FileSet, *ignore.Matcher, config.FolderConfiguration, versioner.Versioner, events.Logger, *byteSemaphore) service 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()), globalRequestLimiter: newByteSemaphore(1024 * cfg.Options().MaxConcurrentIncomingRequestKiB()),
folderIOLimiter: newByteSemaphore(cfg.Options().MaxFolderConcurrency()), folderIOLimiter: newByteSemaphore(cfg.Options().MaxFolderConcurrency()),
fatalChan: make(chan error), fatalChan: make(chan error),
started: make(chan struct{}),
// fields protected by fmut // fields protected by fmut
fmut: sync.NewRWMutex(), fmut: sync.NewRWMutex(),
@ -243,9 +244,6 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio
deviceDownloads: make(map[protocol.DeviceID]*deviceDownloadState), deviceDownloads: make(map[protocol.DeviceID]*deviceDownloadState),
remotePausedFolders: make(map[protocol.DeviceID]map[string]struct{}), remotePausedFolders: make(map[protocol.DeviceID]map[string]struct{}),
indexSenders: make(map[protocol.DeviceID]*indexSenderRegistry), indexSenders: make(map[protocol.DeviceID]*indexSenderRegistry),
// for testing only
started: make(chan struct{}),
} }
for devID := range cfg.Devices() { for devID := range cfg.Devices() {
m.deviceStatRefs[devID] = stats.NewDeviceStatisticsReference(m.db, devID) 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 { func (m *model) serve(ctx context.Context) error {
defer m.closeAllConnectionsAndWait() defer m.closeAllConnectionsAndWait()
m.cfg.Subscribe(m) cfg := m.cfg.Subscribe(m)
defer m.cfg.Unsubscribe(m) defer m.cfg.Unsubscribe(m)
if err := m.initFolders(); err != nil { if err := m.initFolders(cfg); err != nil {
close(m.started) close(m.started)
return svcutil.AsFatalErr(err, svcutil.ExitError) return svcutil.AsFatalErr(err, svcutil.ExitError)
} }
@ -277,17 +275,14 @@ func (m *model) serve(ctx context.Context) error {
} }
} }
func (m *model) initFolders() error { func (m *model) initFolders(cfg config.Configuration) error {
cacheIgnoredFiles := m.cfg.Options().CacheIgnoredFiles clusterConfigDevices := make(deviceIDSet, len(cfg.Devices))
existingDevices := m.cfg.Devices() for _, folderCfg := range cfg.Folders {
existingFolders := m.cfg.Folders()
clusterConfigDevices := make(deviceIDSet, len(existingDevices))
for _, folderCfg := range existingFolders {
if folderCfg.Paused { if folderCfg.Paused {
folderCfg.CreateRoot() folderCfg.CreateRoot()
continue continue
} }
err := m.newFolder(folderCfg, cacheIgnoredFiles) err := m.newFolder(folderCfg, cfg.Options.CacheIgnoredFiles)
if err != nil { if err != nil {
return err return err
} }
@ -295,7 +290,7 @@ func (m *model) initFolders() error {
} }
ignoredDevices := observedDeviceSet(m.cfg.IgnoredDevices()) ignoredDevices := observedDeviceSet(m.cfg.IgnoredDevices())
m.cleanPending(existingDevices, existingFolders, ignoredDevices, nil) m.cleanPending(cfg.DeviceMap(), cfg.FolderMap(), ignoredDevices, nil)
m.resendClusterConfig(clusterConfigDevices.AsSlice()) m.resendClusterConfig(clusterConfigDevices.AsSlice())
return nil 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") panic("bug: ClusterConfig called on closed or nonexistent connection")
} }
changed := false
deviceCfg, ok := m.cfg.Device(deviceID) deviceCfg, ok := m.cfg.Device(deviceID)
if !ok { if !ok {
l.Debugln("Device disappeared from config while processing cluster-config") 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 // Needs to happen outside of the fmut, as can cause CommitConfiguration
if deviceCfg.AutoAcceptFolders { if deviceCfg.AutoAcceptFolders {
changedFolders := make([]config.FolderConfiguration, 0, len(cm.Folders)) w, _ := m.cfg.Modify(func(cfg *config.Configuration) {
for _, folder := range cm.Folders { changedFcfg := make(map[string]config.FolderConfiguration)
if fcfg, fchanged := m.handleAutoAccepts(deviceID, folder, ccDeviceInfos[folder.ID]); fchanged { haveFcfg := cfg.FolderMap()
changedFolders = append(changedFolders, fcfg) 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(changedFcfg) == 0 {
if len(changedFolders) > 0 { return
// Need to wait for the waiter, as this calls CommitConfiguration, }
// which sets up the folder and as we return from this call, for i := range cfg.Folders {
// ClusterConfig starts poking at m.folderFiles and other things if fcfg, ok := changedFcfg[cfg.Folders[i].ID]; ok {
// that might not exist until the config is committed. cfg.Folders[i] = fcfg
w, _ := m.cfg.SetFolders(changedFolders) delete(changedFcfg, cfg.Folders[i].ID)
w.Wait() }
changed = true }
} 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) 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 { if deviceCfg.Introducer {
folders, devices, foldersDevices, introduced := m.handleIntroductions(deviceCfg, cm) m.cfg.Modify(func(cfg *config.Configuration) {
folders, devices, deintroduced := m.handleDeintroductions(deviceCfg, foldersDevices, folders, devices) folders, devices, foldersDevices, introduced := m.handleIntroductions(deviceCfg, cm, cfg.FolderMap(), cfg.DeviceMap())
if introduced || deintroduced { folders, devices, deintroduced := m.handleDeintroductions(deviceCfg, foldersDevices, folders, devices)
changed = true if !introduced && !deintroduced {
cfg := m.cfg.RawCopy() return
}
cfg.Folders = make([]config.FolderConfiguration, 0, len(folders)) cfg.Folders = make([]config.FolderConfiguration, 0, len(folders))
for _, fcfg := range folders { for _, fcfg := range folders {
cfg.Folders = append(cfg.Folders, fcfg) cfg.Folders = append(cfg.Folders, fcfg)
@ -1270,14 +1277,7 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
for _, dcfg := range devices { for _, dcfg := range devices {
cfg.Devices = append(cfg.Devices, dcfg) 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 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 // 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 changed := false
folders := m.cfg.Folders()
devices := m.cfg.Devices()
foldersDevices := make(folderDeviceSet) foldersDevices := make(folderDeviceSet)
@ -1521,7 +1519,7 @@ func (m *model) handleIntroductions(introducerCfg config.DeviceConfiguration, cm
foldersDevices.set(device.ID, folder.ID) 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. // The device is currently unknown. Add it to the config.
devices[device.ID] = m.introduceDevice(device, introducerCfg) devices[device.ID] = m.introduceDevice(device, introducerCfg)
} else if fcfg.SharedWith(device.ID) { } 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 // handleAutoAccepts handles adding and sharing folders for devices that have
// AutoAcceptFolders set to true. // AutoAcceptFolders set to true.
func (m *model) handleAutoAccepts(deviceID protocol.DeviceID, folder protocol.Folder, ccDeviceInfos *indexSenderStartInfo) (config.FolderConfiguration, bool) { func (m *model) handleAutoAccepts(deviceID protocol.DeviceID, folder protocol.Folder, ccDeviceInfos *indexSenderStartInfo, cfg config.FolderConfiguration, haveCfg bool, defaultPath string) (config.FolderConfiguration, bool) {
if cfg, ok := m.cfg.Folder(folder.ID); !ok { if !haveCfg {
defaultPath := m.cfg.Options().DefaultFolderPath
defaultPathFs := fs.NewFilesystem(fs.FilesystemTypeBasic, defaultPath) defaultPathFs := fs.NewFilesystem(fs.FilesystemTypeBasic, defaultPath)
pathAlternatives := []string{ pathAlternatives := []string{
fs.SanitizePath(folder.Label), fs.SanitizePath(folder.Label),
@ -2179,9 +2176,16 @@ func (m *model) AddConnection(conn protocol.Connection, hello protocol.Hello) {
conn.ClusterConfig(cm) conn.ClusterConfig(cm)
if (device.Name == "" || m.cfg.Options().OverwriteRemoteDevNames) && hello.DeviceName != "" { if (device.Name == "" || m.cfg.Options().OverwriteRemoteDevNames) && hello.DeviceName != "" {
device.Name = hello.DeviceName m.cfg.Modify(func(cfg *config.Configuration) {
m.cfg.SetDevice(device) for i := range cfg.Devices {
m.cfg.Save() if cfg.Devices[i].DeviceID == deviceID {
if cfg.Devices[i].Name == "" || cfg.Options.OverwriteRemoteDevNames {
cfg.Devices[i].Name = hello.DeviceName
}
return
}
}
})
} }
m.deviceWasSeen(deviceID) 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 { 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. // 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. // 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. // 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) w := evLogger.Subscribe(events.DownloadProgress)
c := createTmpWrapper(config.Configuration{}) c, cfgCancel := createTmpWrapper(config.Configuration{})
defer os.Remove(c.ConfigPath()) defer os.Remove(c.ConfigPath())
c.SetOptions(config.OptionsConfiguration{ defer cfgCancel()
ProgressUpdateIntervalS: 60, // irrelevant, but must be positive 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) p := NewProgressEmitter(c, evLogger)
go p.Serve(ctx) go p.Serve(ctx)
@ -109,12 +114,17 @@ func TestProgressEmitter(t *testing.T) {
} }
func TestSendDownloadProgressMessages(t *testing.T) { func TestSendDownloadProgressMessages(t *testing.T) {
c := createTmpWrapper(config.Configuration{}) c, cfgCancel := createTmpWrapper(config.Configuration{})
defer os.Remove(c.ConfigPath()) defer os.Remove(c.ConfigPath())
c.SetOptions(config.OptionsConfiguration{ defer cfgCancel()
ProgressUpdateIntervalS: 60, // irrelevant, but must be positive waiter, err := c.Modify(func(cfg *config.Configuration) {
TempIndexMinBlocks: 10, cfg.Options.ProgressUpdateIntervalS = 60 // irrelevant, but must be positive
cfg.Options.TempIndexMinBlocks = 10
}) })
if err != nil {
t.Fatal(err)
}
waiter.Wait()
fc := &fakeConnection{} 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 // Verify that the model performs a request and creates a file based on
// an incoming index update. // an incoming index update.
m, fc, fcfg := setupModelWithConnection(t) m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
tfs := fcfg.Filesystem() tfs := fcfg.Filesystem()
defer cleanupModelAndRemoveDir(m, tfs.URI()) defer cleanupModelAndRemoveDir(m, tfs.URI())
@ -72,7 +73,8 @@ func TestSymlinkTraversalRead(t *testing.T) {
return return
} }
m, fc, fcfg := setupModelWithConnection(t) m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI()) defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
// We listen for incoming index updates and trigger when we see one for // We listen for incoming index updates and trigger when we see one for
@ -115,7 +117,8 @@ func TestSymlinkTraversalWrite(t *testing.T) {
return return
} }
m, fc, fcfg := setupModelWithConnection(t) m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI()) defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
// We listen for incoming index updates and trigger when we see one for // 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) { func TestRequestCreateTmpSymlink(t *testing.T) {
// Test that an update for a temporary file is invalidated // 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()) defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
// We listen for incoming index updates and trigger when we see one for // 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 // Sets up a folder with trashcan versioning and tries to use a
// deleted symlink to escape // deleted symlink to escape
w, fcfg := tmpDefaultWrapper() w, fcfg, wCancel := tmpDefaultWrapper()
defer wCancel()
defer func() { defer func() {
os.RemoveAll(fcfg.Filesystem().URI()) os.RemoveAll(fcfg.Filesystem().URI())
os.Remove(w.ConfigPath()) os.Remove(w.ConfigPath())
}() }()
fcfg.Versioning = config.VersioningConfiguration{Type: "trashcan"} fcfg.Versioning = config.VersioningConfiguration{Type: "trashcan"}
w.SetFolder(fcfg) setFolder(t, w, fcfg)
m, fc := setupModelWithConnectionFromWrapper(t, w) m, fc := setupModelWithConnectionFromWrapper(t, w)
defer cleanupModel(m) 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. // This test checks that (un-)ignored/invalid/deleted files are treated as expected.
func pullInvalidIgnored(t *testing.T, ft config.FolderType) { func pullInvalidIgnored(t *testing.T, ft config.FolderType) {
w := createTmpWrapper(defaultCfgWrapper.RawCopy()) w, wCancel := createTmpWrapper(defaultCfgWrapper.RawCopy())
defer wCancel()
fcfg := testFolderConfigTmp() fcfg := testFolderConfigTmp()
fss := fcfg.Filesystem() fss := fcfg.Filesystem()
fcfg.Type = ft fcfg.Type = ft
w.SetFolder(fcfg) setFolder(t, w, fcfg)
m := setupModel(t, w) m := setupModel(t, w)
defer cleanupModelAndRemoveDir(m, fss.URI()) defer cleanupModelAndRemoveDir(m, fss.URI())
@ -420,7 +426,8 @@ func pullInvalidIgnored(t *testing.T, ft config.FolderType) {
} }
func TestIssue4841(t *testing.T) { func TestIssue4841(t *testing.T) {
m, fc, fcfg := setupModelWithConnection(t) m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI()) defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
received := make(chan []protocol.FileInfo) received := make(chan []protocol.FileInfo)
@ -464,7 +471,8 @@ func TestIssue4841(t *testing.T) {
} }
func TestRescanIfHaveInvalidContent(t *testing.T) { func TestRescanIfHaveInvalidContent(t *testing.T) {
m, fc, fcfg := setupModelWithConnection(t) m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
tfs := fcfg.Filesystem() tfs := fcfg.Filesystem()
defer cleanupModelAndRemoveDir(m, tfs.URI()) defer cleanupModelAndRemoveDir(m, tfs.URI())
@ -530,7 +538,8 @@ func TestRescanIfHaveInvalidContent(t *testing.T) {
} }
func TestParentDeletion(t *testing.T) { func TestParentDeletion(t *testing.T) {
m, fc, fcfg := setupModelWithConnection(t) m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
testFs := fcfg.Filesystem() testFs := fcfg.Filesystem()
defer cleanupModelAndRemoveDir(m, testFs.URI()) defer cleanupModelAndRemoveDir(m, testFs.URI())
@ -609,7 +618,8 @@ func TestRequestSymlinkWindows(t *testing.T) {
t.Skip("windows specific test") t.Skip("windows specific test")
} }
m, fc, fcfg := setupModelWithConnection(t) m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI()) defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
received := make(chan []protocol.FileInfo) received := make(chan []protocol.FileInfo)
@ -677,7 +687,8 @@ func equalContents(path string, contents []byte) error {
} }
func TestRequestRemoteRenameChanged(t *testing.T) { func TestRequestRemoteRenameChanged(t *testing.T) {
m, fc, fcfg := setupModelWithConnection(t) m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
tfs := fcfg.Filesystem() tfs := fcfg.Filesystem()
tmpDir := tfs.URI() tmpDir := tfs.URI()
defer cleanupModelAndRemoveDir(m, tfs.URI()) defer cleanupModelAndRemoveDir(m, tfs.URI())
@ -812,7 +823,8 @@ func TestRequestRemoteRenameChanged(t *testing.T) {
} }
func TestRequestRemoteRenameConflict(t *testing.T) { func TestRequestRemoteRenameConflict(t *testing.T) {
m, fc, fcfg := setupModelWithConnection(t) m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
tfs := fcfg.Filesystem() tfs := fcfg.Filesystem()
tmpDir := tfs.URI() tmpDir := tfs.URI()
defer cleanupModelAndRemoveDir(m, tmpDir) defer cleanupModelAndRemoveDir(m, tmpDir)
@ -903,7 +915,8 @@ func TestRequestRemoteRenameConflict(t *testing.T) {
} }
func TestRequestDeleteChanged(t *testing.T) { func TestRequestDeleteChanged(t *testing.T) {
m, fc, fcfg := setupModelWithConnection(t) m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
tfs := fcfg.Filesystem() tfs := fcfg.Filesystem()
defer cleanupModelAndRemoveDir(m, tfs.URI()) defer cleanupModelAndRemoveDir(m, tfs.URI())
@ -972,7 +985,8 @@ func TestRequestDeleteChanged(t *testing.T) {
} }
func TestNeedFolderFiles(t *testing.T) { func TestNeedFolderFiles(t *testing.T) {
m, fc, fcfg := setupModelWithConnection(t) m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
tfs := fcfg.Filesystem() tfs := fcfg.Filesystem()
tmpDir := tfs.URI() tmpDir := tfs.URI()
defer cleanupModelAndRemoveDir(m, tmpDir) defer cleanupModelAndRemoveDir(m, tmpDir)
@ -1020,7 +1034,8 @@ func TestNeedFolderFiles(t *testing.T) {
// propagated upon un-ignoring. // propagated upon un-ignoring.
// https://github.com/syncthing/syncthing/issues/6038 // https://github.com/syncthing/syncthing/issues/6038
func TestIgnoreDeleteUnignore(t *testing.T) { func TestIgnoreDeleteUnignore(t *testing.T) {
w, fcfg := tmpDefaultWrapper() w, fcfg, wCancel := tmpDefaultWrapper()
defer wCancel()
m := setupModel(t, w) m := setupModel(t, w)
fss := fcfg.Filesystem() fss := fcfg.Filesystem()
tmpDir := fss.URI() tmpDir := fss.URI()
@ -1120,7 +1135,8 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
// TestRequestLastFileProgress checks that the last pulled file (here only) is registered // TestRequestLastFileProgress checks that the last pulled file (here only) is registered
// as in progress. // as in progress.
func TestRequestLastFileProgress(t *testing.T) { func TestRequestLastFileProgress(t *testing.T) {
m, fc, fcfg := setupModelWithConnection(t) m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
tfs := fcfg.Filesystem() tfs := fcfg.Filesystem()
defer cleanupModelAndRemoveDir(m, tfs.URI()) defer cleanupModelAndRemoveDir(m, tfs.URI())
@ -1156,7 +1172,8 @@ func TestRequestIndexSenderPause(t *testing.T) {
done := make(chan struct{}) done := make(chan struct{})
defer close(done) defer close(done)
m, fc, fcfg := setupModelWithConnection(t) m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
tfs := fcfg.Filesystem() tfs := fcfg.Filesystem()
defer cleanupModelAndRemoveDir(m, tfs.URI()) defer cleanupModelAndRemoveDir(m, tfs.URI())
@ -1215,13 +1232,8 @@ func TestRequestIndexSenderPause(t *testing.T) {
// Local paused and resume // Local paused and resume
fcfg.Paused = true pauseFolder(t, m.cfg, fcfg.ID, true)
waiter, _ := m.cfg.SetFolder(fcfg) pauseFolder(t, m.cfg, fcfg.ID, false)
waiter.Wait()
fcfg.Paused = false
waiter, _ = m.cfg.SetFolder(fcfg)
waiter.Wait()
seq++ seq++
files[0].Sequence = seq files[0].Sequence = seq
@ -1238,16 +1250,12 @@ func TestRequestIndexSenderPause(t *testing.T) {
cc.Folders[0].Paused = true cc.Folders[0].Paused = true
m.ClusterConfig(device1, cc) m.ClusterConfig(device1, cc)
fcfg.Paused = true pauseFolder(t, m.cfg, fcfg.ID, true)
waiter, _ = m.cfg.SetFolder(fcfg)
waiter.Wait()
cc.Folders[0].Paused = false cc.Folders[0].Paused = false
m.ClusterConfig(device1, cc) m.ClusterConfig(device1, cc)
fcfg.Paused = false pauseFolder(t, m.cfg, fcfg.ID, false)
waiter, _ = m.cfg.SetFolder(fcfg)
waiter.Wait()
seq++ seq++
files[0].Sequence = seq files[0].Sequence = seq
@ -1277,7 +1285,8 @@ func TestRequestIndexSenderPause(t *testing.T) {
} }
func TestRequestIndexSenderClusterConfigBeforeStart(t *testing.T) { func TestRequestIndexSenderClusterConfigBeforeStart(t *testing.T) {
w, fcfg := tmpDefaultWrapper() w, fcfg, wCancel := tmpDefaultWrapper()
defer wCancel()
tfs := fcfg.Filesystem() tfs := fcfg.Filesystem()
dir1 := "foo" dir1 := "foo"
dir2 := "bar" dir2 := "bar"
@ -1340,12 +1349,11 @@ func TestRequestIndexSenderClusterConfigBeforeStart(t *testing.T) {
} }
func TestRequestReceiveEncryptedLocalNoSend(t *testing.T) { func TestRequestReceiveEncryptedLocalNoSend(t *testing.T) {
w, fcfg := tmpDefaultWrapper() w, fcfg, wCancel := tmpDefaultWrapper()
defer wCancel()
tfs := fcfg.Filesystem() tfs := fcfg.Filesystem()
fcfg.Type = config.FolderTypeReceiveEncrypted fcfg.Type = config.FolderTypeReceiveEncrypted
waiter, err := w.SetFolder(fcfg) setFolder(t, w, fcfg)
must(t, err)
waiter.Wait()
encToken := protocol.PasswordToken(fcfg.ID, "pw") encToken := protocol.PasswordToken(fcfg.ID, "pw")
must(t, tfs.Mkdir(config.DefaultMarkerName, 0777)) must(t, tfs.Mkdir(config.DefaultMarkerName, 0777))

View File

@ -13,6 +13,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/thejerf/suture/v4"
"github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db" "github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/db/backend" "github.com/syncthing/syncthing/lib/db/backend"
@ -24,12 +26,13 @@ import (
) )
var ( var (
myID, device1, device2 protocol.DeviceID myID, device1, device2 protocol.DeviceID
defaultCfgWrapper config.Wrapper defaultCfgWrapper config.Wrapper
defaultFolderConfig config.FolderConfiguration defaultCfgWrapperCancel context.CancelFunc
defaultFs fs.Filesystem defaultFolderConfig config.FolderConfiguration
defaultCfg config.Configuration defaultFs fs.Filesystem
defaultAutoAcceptCfg config.Configuration defaultCfg config.Configuration
defaultAutoAcceptCfg config.Configuration
) )
func init() { func init() {
@ -40,12 +43,13 @@ func init() {
defaultFolderConfig = testFolderConfig("testdata") defaultFolderConfig = testFolderConfig("testdata")
defaultFs = defaultFolderConfig.Filesystem() defaultFs = defaultFolderConfig.Filesystem()
defaultCfgWrapper = createTmpWrapper(config.New(myID)) defaultCfgWrapper, defaultCfgWrapperCancel = createTmpWrapper(config.New(myID))
_, _ = defaultCfgWrapper.SetDevice(config.NewDeviceConfiguration(device1, "device1")) waiter, _ := defaultCfgWrapper.Modify(func(cfg *config.Configuration) {
_, _ = defaultCfgWrapper.SetFolder(defaultFolderConfig) cfg.SetDevice(config.NewDeviceConfiguration(device1, "device1"))
opts := defaultCfgWrapper.Options() cfg.SetFolder(defaultFolderConfig)
opts.KeepTemporariesH = 1 cfg.Options.KeepTemporariesH = 1
_, _ = defaultCfgWrapper.SetOptions(opts) })
waiter.Wait()
defaultCfg = defaultCfgWrapper.RawCopy() defaultCfg = defaultCfgWrapper.RawCopy()
@ -70,11 +74,28 @@ func init() {
} }
} }
func tmpDefaultWrapper() (config.Wrapper, config.FolderConfiguration) { func createTmpWrapper(cfg config.Configuration) (config.Wrapper, context.CancelFunc) {
w := createTmpWrapper(defaultCfgWrapper.RawCopy()) 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() fcfg := testFolderConfigTmp()
_, _ = w.SetFolder(fcfg) _, _ = w.Modify(func(cfg *config.Configuration) {
return w, fcfg cfg.SetFolder(fcfg)
})
return w, fcfg, cancel
} }
func testFolderConfigTmp() config.FolderConfiguration { func testFolderConfigTmp() config.FolderConfiguration {
@ -96,11 +117,11 @@ func testFolderConfigFake() config.FolderConfiguration {
return cfg return cfg
} }
func setupModelWithConnection(t testing.TB) (*testModel, *fakeConnection, config.FolderConfiguration) { func setupModelWithConnection(t testing.TB) (*testModel, *fakeConnection, config.FolderConfiguration, context.CancelFunc) {
t.Helper() t.Helper()
w, fcfg := tmpDefaultWrapper() w, fcfg, cancel := tmpDefaultWrapper()
m, fc := setupModelWithConnectionFromWrapper(t, w) m, fc := setupModelWithConnectionFromWrapper(t, w)
return m, fc, fcfg return m, fc, fcfg, cancel
} }
func setupModelWithConnectionFromWrapper(t testing.TB, w config.Wrapper) (*testModel, *fakeConnection) { 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 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 { 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(ur.NewFailureHandler(a.cfg, a.evLogger))
a.mainService.Add(a.ll) a.mainService.Add(a.ll)
@ -277,24 +281,21 @@ func (a *App) startup() error {
a.mainService.Add(discoveryManager) a.mainService.Add(discoveryManager)
a.mainService.Add(connectionsService) a.mainService.Add(connectionsService)
// Candidate builds always run with usage reporting. a.cfg.Modify(func(cfg *config.Configuration) {
// Candidate builds always run with usage reporting.
if opts := a.cfg.Options(); build.IsCandidate { if build.IsCandidate {
l.Infoln("Anonymous usage reporting is always enabled for candidate releases.") l.Infoln("Anonymous usage reporting is always enabled for candidate releases.")
if opts.URAccepted != ur.Version { if cfg.Options.URAccepted != ur.Version {
opts.URAccepted = ur.Version cfg.Options.URAccepted = ur.Version
a.cfg.SetOptions(opts) // Unique ID will be set and config saved below if necessary.
a.cfg.Save() }
// 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 we are going to do usage reporting, ensure we have a valid unique ID.
if opts := a.cfg.Options(); opts.URAccepted > 0 && opts.URUniqueID == "" { if cfg.Options.URAccepted > 0 && cfg.Options.URUniqueID == "" {
opts.URUniqueID = rand.String(8) cfg.Options.URUniqueID = rand.String(8)
a.cfg.SetOptions(opts) }
a.cfg.Save() })
}
usageReportingSvc := ur.New(a.cfg, m, connectionsService, a.opts.NoUpgrade) usageReportingSvc := ur.New(a.cfg, m, connectionsService, a.opts.NoUpgrade)
a.mainService.Add(usageReportingSvc) a.mainService.Add(usageReportingSvc)

View File

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