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
20 changed files with 1032 additions and 790 deletions
+17 -12
View File
@@ -41,10 +41,20 @@ func (validationError) String() string {
return "validationError"
}
func TestReplaceCommit(t *testing.T) {
t.Skip("broken, fails randomly, #3834")
func replace(t testing.TB, w Wrapper, to Configuration) {
t.Helper()
waiter, err := w.Modify(func(cfg *Configuration) {
*cfg = to
})
if err != nil {
t.Fatal(err)
}
waiter.Wait()
}
func TestReplaceCommit(t *testing.T) {
w := wrap("/dev/null", Configuration{Version: 0}, device1)
defer w.stop()
if w.RawCopy().Version != 0 {
t.Fatal("Config incorrect")
}
@@ -52,10 +62,7 @@ func TestReplaceCommit(t *testing.T) {
// Replace config. We should get back a clean response and the config
// should change.
_, err := w.Replace(Configuration{Version: 1})
if err != nil {
t.Fatal("Should not have a validation error:", err)
}
replace(t, w, Configuration{Version: 1})
if w.RequiresRestart() {
t.Fatal("Should not require restart")
}
@@ -69,11 +76,7 @@ func TestReplaceCommit(t *testing.T) {
sub0 := requiresRestart{committed: make(chan struct{}, 1)}
w.Subscribe(sub0)
_, err = w.Replace(Configuration{Version: 2})
if err != nil {
t.Fatal("Should not have a validation error:", err)
}
replace(t, w, Configuration{Version: 1})
<-sub0.committed
if !w.RequiresRestart() {
t.Fatal("Should require restart")
@@ -87,7 +90,9 @@ func TestReplaceCommit(t *testing.T) {
w.Subscribe(validationError{})
_, err = w.Replace(Configuration{Version: 3})
_, err := w.Modify(func(cfg *Configuration) {
*cfg = Configuration{Version: 3}
})
if err == nil {
t.Fatal("Should have a validation error")
}
+67
View File
@@ -372,6 +372,15 @@ func (cfg *Configuration) applyMigrations() {
migrationsMut.Unlock()
}
func (cfg *Configuration) Device(id protocol.DeviceID) (DeviceConfiguration, int, bool) {
for i, device := range cfg.Devices {
if device.DeviceID == id {
return device, i, true
}
}
return DeviceConfiguration{}, 0, false
}
// DeviceMap returns a map of device ID to device configuration for the given configuration.
func (cfg *Configuration) DeviceMap() map[protocol.DeviceID]DeviceConfiguration {
m := make(map[protocol.DeviceID]DeviceConfiguration, len(cfg.Devices))
@@ -381,6 +390,44 @@ func (cfg *Configuration) DeviceMap() map[protocol.DeviceID]DeviceConfiguration
return m
}
func (cfg *Configuration) SetDevice(device DeviceConfiguration) {
cfg.SetDevices([]DeviceConfiguration{device})
}
func (cfg *Configuration) SetDevices(devices []DeviceConfiguration) {
inds := make(map[protocol.DeviceID]int, len(cfg.Devices))
for i, device := range cfg.Devices {
inds[device.DeviceID] = i
}
filtered := devices[:0]
for _, device := range devices {
if i, ok := inds[device.DeviceID]; ok {
cfg.Devices[i] = device
} else {
filtered = append(filtered, device)
}
}
cfg.Devices = append(cfg.Devices, filtered...)
}
func (cfg *Configuration) Folder(id string) (FolderConfiguration, int, bool) {
for i, folder := range cfg.Folders {
if folder.ID == id {
return folder, i, true
}
}
return FolderConfiguration{}, 0, false
}
// FolderMap returns a map of folder ID to folder configuration for the given configuration.
func (cfg *Configuration) FolderMap() map[string]FolderConfiguration {
m := make(map[string]FolderConfiguration, len(cfg.Folders))
for _, folder := range cfg.Folders {
m[folder.ID] = folder
}
return m
}
// FolderPasswords returns the folder passwords set for this device, for
// folders that have an encryption password set.
func (cfg Configuration) FolderPasswords(device protocol.DeviceID) map[string]string {
@@ -397,6 +444,26 @@ nextFolder:
return res
}
func (cfg *Configuration) SetFolder(folder FolderConfiguration) {
cfg.SetFolders([]FolderConfiguration{folder})
}
func (cfg *Configuration) SetFolders(folders []FolderConfiguration) {
inds := make(map[string]int, len(cfg.Folders))
for i, folder := range cfg.Folders {
inds[folder.ID] = i
}
filtered := folders[:0]
for _, folder := range folders {
if i, ok := inds[folder.ID]; ok {
cfg.Folders[i] = folder
} else {
filtered = append(filtered, folder)
}
}
cfg.Folders = append(cfg.Folders, filtered...)
}
func ensureDevicePresent(devices []FolderDeviceConfiguration, myID protocol.DeviceID) []FolderDeviceConfiguration {
for _, device := range devices {
if device.DeviceID.Equals(myID) {
+122 -33
View File
@@ -8,9 +8,11 @@ package config
import (
"bytes"
"context"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
@@ -21,6 +23,7 @@ import (
"testing"
"github.com/d4l3k/messagediff"
"github.com/thejerf/suture/v4"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/fs"
@@ -95,7 +98,8 @@ func TestDeviceConfig(t *testing.T) {
}
os.RemoveAll(filepath.Join("testdata", DefaultMarkerName))
wr, err := load(cfgFile, device1)
wr, wrCancel, err := copyAndLoad(cfgFile, device1)
defer wrCancel()
if err != nil {
t.Fatal(err)
}
@@ -107,7 +111,7 @@ func TestDeviceConfig(t *testing.T) {
t.Fatal("Unexpected file")
}
cfg := wr.(*wrapper).cfg
cfg := wr.Wrapper.(*wrapper).cfg
expectedFolders := []FolderConfiguration{
{
@@ -170,7 +174,8 @@ func TestDeviceConfig(t *testing.T) {
}
func TestNoListenAddresses(t *testing.T) {
cfg, err := load("testdata/nolistenaddress.xml", device1)
cfg, cfgCancel, err := copyAndLoad("testdata/nolistenaddress.xml", device1)
defer cfgCancel()
if err != nil {
t.Error(err)
}
@@ -228,7 +233,8 @@ func TestOverriddenValues(t *testing.T) {
}
os.Unsetenv("STNOUPGRADE")
cfg, err := load("testdata/overridenvalues.xml", device1)
cfg, cfgCancel, err := copyAndLoad("testdata/overridenvalues.xml", device1)
defer cfgCancel()
if err != nil {
t.Error(err)
}
@@ -269,7 +275,8 @@ func TestDeviceAddressesDynamic(t *testing.T) {
},
}
cfg, err := load("testdata/deviceaddressesdynamic.xml", device4)
cfg, cfgCancel, err := copyAndLoad("testdata/deviceaddressesdynamic.xml", device4)
defer cfgCancel()
if err != nil {
t.Error(err)
}
@@ -314,7 +321,8 @@ func TestDeviceCompression(t *testing.T) {
},
}
cfg, err := load("testdata/devicecompression.xml", device4)
cfg, cfgCancel, err := copyAndLoad("testdata/devicecompression.xml", device4)
defer cfgCancel()
if err != nil {
t.Error(err)
}
@@ -356,7 +364,8 @@ func TestDeviceAddressesStatic(t *testing.T) {
},
}
cfg, err := load("testdata/deviceaddressesstatic.xml", device4)
cfg, cfgCancel, err := copyAndLoad("testdata/deviceaddressesstatic.xml", device4)
defer cfgCancel()
if err != nil {
t.Error(err)
}
@@ -368,7 +377,8 @@ func TestDeviceAddressesStatic(t *testing.T) {
}
func TestVersioningConfig(t *testing.T) {
cfg, err := load("testdata/versioningconfig.xml", device4)
cfg, cfgCancel, err := copyAndLoad("testdata/versioningconfig.xml", device4)
defer cfgCancel()
if err != nil {
t.Error(err)
}
@@ -395,7 +405,8 @@ func TestIssue1262(t *testing.T) {
t.Skipf("path gets converted to absolute as part of the filesystem initialization on linux")
}
cfg, err := load("testdata/issue-1262.xml", device4)
cfg, cfgCancel, err := copyAndLoad("testdata/issue-1262.xml", device4)
defer cfgCancel()
if err != nil {
t.Fatal(err)
}
@@ -409,7 +420,8 @@ func TestIssue1262(t *testing.T) {
}
func TestIssue1750(t *testing.T) {
cfg, err := load("testdata/issue-1750.xml", device4)
cfg, cfgCancel, err := copyAndLoad("testdata/issue-1750.xml", device4)
defer cfgCancel()
if err != nil {
t.Fatal(err)
}
@@ -505,6 +517,7 @@ func TestFolderCheckPath(t *testing.T) {
func TestNewSaveLoad(t *testing.T) {
path := "testdata/temp.xml"
os.Remove(path)
defer os.Remove(path)
exists := func(path string) bool {
_, err := os.Stat(path)
@@ -513,6 +526,7 @@ func TestNewSaveLoad(t *testing.T) {
intCfg := New(device1)
cfg := wrap(path, intCfg, device1)
defer cfg.stop()
if exists(path) {
t.Error(path, "exists")
@@ -527,6 +541,7 @@ func TestNewSaveLoad(t *testing.T) {
}
cfg2, err := load(path, device1)
defer cfg2.stop()
if err != nil {
t.Error(err)
}
@@ -534,8 +549,6 @@ func TestNewSaveLoad(t *testing.T) {
if diff, equal := messagediff.PrettyDiff(cfg.RawCopy(), cfg2.RawCopy()); !equal {
t.Errorf("Configs are not equal. Diff:\n%s", diff)
}
os.Remove(path)
}
func TestPrepare(t *testing.T) {
@@ -553,7 +566,8 @@ func TestPrepare(t *testing.T) {
}
func TestCopy(t *testing.T) {
wrapper, err := load("testdata/example.xml", device1)
wrapper, wrapperCancel, err := copyAndLoad("testdata/example.xml", device1)
defer wrapperCancel()
if err != nil {
t.Fatal(err)
}
@@ -592,7 +606,8 @@ func TestCopy(t *testing.T) {
}
func TestPullOrder(t *testing.T) {
wrapper, err := load("testdata/pullorder.xml", device1)
wrapper, wrapperCleanup, err := copyAndLoad("testdata/pullorder.xml", device1)
defer wrapperCleanup()
if err != nil {
t.Fatal(err)
}
@@ -632,8 +647,9 @@ func TestPullOrder(t *testing.T) {
if err != nil {
t.Fatal(err)
}
wrapper = wrap("testdata/pullorder.xml", cfg, device1)
folders = wrapper.Folders()
wrapper2 := wrap(wrapper.ConfigPath(), cfg, device1)
defer wrapper2.stop()
folders = wrapper2.Folders()
for _, tc := range expected {
if actual := folders[tc.name].Order; actual != tc.order {
@@ -643,7 +659,8 @@ func TestPullOrder(t *testing.T) {
}
func TestLargeRescanInterval(t *testing.T) {
wrapper, err := load("testdata/largeinterval.xml", device1)
wrapper, wrapperCancel, err := copyAndLoad("testdata/largeinterval.xml", device1)
defer wrapperCancel()
if err != nil {
t.Fatal(err)
}
@@ -681,7 +698,8 @@ func TestGUIConfigURL(t *testing.T) {
func TestDuplicateDevices(t *testing.T) {
// Duplicate devices should be removed
wrapper, err := load("testdata/dupdevices.xml", device1)
wrapper, wrapperCancel, err := copyAndLoad("testdata/dupdevices.xml", device1)
defer wrapperCancel()
if err != nil {
t.Fatal(err)
}
@@ -699,7 +717,8 @@ func TestDuplicateDevices(t *testing.T) {
func TestDuplicateFolders(t *testing.T) {
// Duplicate folders are a loading error
_, err := load("testdata/dupfolders.xml", device1)
_, _Cancel, err := copyAndLoad("testdata/dupfolders.xml", device1)
defer _Cancel()
if err == nil || !strings.Contains(err.Error(), errFolderIDDuplicate.Error()) {
t.Fatal(`Expected error to mention "duplicate folder ID":`, err)
}
@@ -710,7 +729,8 @@ func TestEmptyFolderPaths(t *testing.T) {
// get messed up by the prepare steps (e.g., become the current dir or
// get a slash added so that it becomes the root directory or similar).
_, err := load("testdata/nopath.xml", device1)
_, _Cancel, err := copyAndLoad("testdata/nopath.xml", device1)
defer _Cancel()
if err == nil || !strings.Contains(err.Error(), errFolderPathEmpty.Error()) {
t.Fatal("Expected error due to empty folder path, got", err)
}
@@ -779,7 +799,8 @@ func TestIgnoredDevices(t *testing.T) {
// Verify that ignored devices that are also present in the
// configuration are not in fact ignored.
wrapper, err := load("testdata/ignoreddevices.xml", device1)
wrapper, wrapperCancel, err := copyAndLoad("testdata/ignoreddevices.xml", device1)
defer wrapperCancel()
if err != nil {
t.Fatal(err)
}
@@ -797,7 +818,8 @@ func TestIgnoredFolders(t *testing.T) {
// configuration are not in fact ignored.
// Also, verify that folders that are shared with a device are not ignored.
wrapper, err := load("testdata/ignoredfolders.xml", device1)
wrapper, wrapperCancel, err := copyAndLoad("testdata/ignoredfolders.xml", device1)
defer wrapperCancel()
if err != nil {
t.Fatal(err)
}
@@ -833,7 +855,8 @@ func TestIgnoredFolders(t *testing.T) {
func TestGetDevice(t *testing.T) {
// Verify that the Device() call does the right thing
wrapper, err := load("testdata/ignoreddevices.xml", device1)
wrapper, wrapperCancel, err := copyAndLoad("testdata/ignoreddevices.xml", device1)
defer wrapperCancel()
if err != nil {
t.Fatal(err)
}
@@ -860,7 +883,8 @@ func TestGetDevice(t *testing.T) {
}
func TestSharesRemovedOnDeviceRemoval(t *testing.T) {
wrapper, err := load("testdata/example.xml", device1)
wrapper, wrapperCancel, err := copyAndLoad("testdata/example.xml", device1)
defer wrapperCancel()
if err != nil {
t.Errorf("Failed: %s", err)
}
@@ -872,10 +896,7 @@ func TestSharesRemovedOnDeviceRemoval(t *testing.T) {
t.Error("Should have less devices")
}
_, err = wrapper.Replace(raw)
if err != nil {
t.Errorf("Failed: %s", err)
}
replace(t, wrapper, raw)
raw = wrapper.RawCopy()
if len(raw.Folders[0].Devices) > len(raw.Devices) {
@@ -947,6 +968,7 @@ func TestIssue4219(t *testing.T) {
}
w := wrap("/tmp/cfg", cfg, myID)
defer w.stop()
if !w.IgnoredFolder(device2, "t1") {
t.Error("Folder device2 t1 should be ignored")
}
@@ -1157,13 +1179,80 @@ func defaultConfigAsMap() map[string]interface{} {
return tmp
}
func load(path string, myID protocol.DeviceID) (Wrapper, error) {
cfg, _, err := Load(path, myID, events.NoopLogger)
return cfg, err
func copyToTmp(path string) (string, error) {
orig, err := os.Open(path)
if err != nil {
return "", err
}
defer orig.Close()
temp, err := ioutil.TempFile("", "syncthing-configTest-")
if err != nil {
return "", err
}
defer temp.Close()
if _, err := io.Copy(temp, orig); err != nil {
return "", err
}
return temp.Name(), nil
}
func wrap(path string, cfg Configuration, myID protocol.DeviceID) Wrapper {
return Wrap(path, cfg, myID, events.NoopLogger)
func copyAndLoad(path string, myID protocol.DeviceID) (*testWrapper, func(), error) {
temp, err := copyToTmp(path)
if err != nil {
return nil, func() {}, err
}
wrapper, err := load(temp, myID)
if err != nil {
return nil, func() {}, err
}
return wrapper, func() {
wrapper.stop()
os.Remove(temp)
}, nil
}
func load(path string, myID protocol.DeviceID) (*testWrapper, error) {
cfg, _, err := Load(path, myID, events.NoopLogger)
if err != nil {
return nil, err
}
return startWrapper(cfg), nil
}
func wrap(path string, cfg Configuration, myID protocol.DeviceID) *testWrapper {
wrapper := Wrap(path, cfg, myID, events.NoopLogger)
return startWrapper(wrapper)
}
type testWrapper struct {
Wrapper
cancel context.CancelFunc
done chan struct{}
}
func (w *testWrapper) stop() {
w.cancel()
<-w.done
}
func startWrapper(wrapper Wrapper) *testWrapper {
tw := &testWrapper{
Wrapper: wrapper,
done: make(chan struct{}),
}
s, ok := wrapper.(suture.Service)
if !ok {
tw.cancel = func() {}
close(tw.done)
return tw
}
ctx, cancel := context.WithCancel(context.Background())
tw.cancel = cancel
go func() {
s.Serve(ctx)
close(tw.done)
}()
return tw
}
func TestInternalVersioningConfiguration(t *testing.T) {
+148 -145
View File
@@ -7,8 +7,12 @@
package config
import (
"context"
"errors"
"os"
"reflect"
"sync/atomic"
"time"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/osutil"
@@ -16,6 +20,13 @@ import (
"github.com/syncthing/syncthing/lib/sync"
)
const (
maxModifications = 1000
minSaveInterval = 5 * time.Second
)
var errTooManyModifications = errors.New("too many concurrent config modifications")
// The Committer interface is implemented by objects that need to know about
// or have a say in configuration changes.
//
@@ -35,6 +46,10 @@ import (
// false will result in a "restart needed" response to the API/user. Note that
// the new configuration will still have been applied by those who were
// capable of doing so.
//
// A Committer must take care not to hold any locks while changing the
// configuration (e.g. calling Wrapper.SetFolder), that are also acquired in any
// methods of the Committer interface.
type Committer interface {
VerifyConfiguration(from, to Configuration) error
CommitConfiguration(from, to Configuration) (handled bool)
@@ -50,45 +65,47 @@ type noopWaiter struct{}
func (noopWaiter) Wait() {}
// A Wrapper around a Configuration that manages loads, saves and published
// notifications of changes to registered Handlers
// ModifyFunction gets a pointer to a copy of the currently active configuration
// for modification.
type ModifyFunction func(*Configuration)
// Wrapper handles a Configuration, i.e. it provides methods to access, change
// and save the config, and notifies registered subscribers (Committer) of
// changes.
//
// Modify allows changing the currently active configuration through the given
// ModifyFunction. It can be called concurrently: All calls will be queued and
// called in order.
type Wrapper interface {
ConfigPath() string
MyID() protocol.DeviceID
RawCopy() Configuration
Replace(cfg Configuration) (Waiter, error)
RequiresRestart() bool
Save() error
GUI() GUIConfiguration
SetGUI(gui GUIConfiguration) (Waiter, error)
LDAP() LDAPConfiguration
SetLDAP(ldap LDAPConfiguration) (Waiter, error)
Modify(ModifyFunction) (Waiter, error)
RemoveFolder(id string) (Waiter, error)
RemoveDevice(id protocol.DeviceID) (Waiter, error)
GUI() GUIConfiguration
LDAP() LDAPConfiguration
Options() OptionsConfiguration
SetOptions(opts OptionsConfiguration) (Waiter, error)
Folder(id string) (FolderConfiguration, bool)
Folders() map[string]FolderConfiguration
FolderList() []FolderConfiguration
RemoveFolder(id string) (Waiter, error)
SetFolder(fld FolderConfiguration) (Waiter, error)
SetFolders(folders []FolderConfiguration) (Waiter, error)
FolderPasswords(device protocol.DeviceID) map[string]string
Device(id protocol.DeviceID) (DeviceConfiguration, bool)
Devices() map[protocol.DeviceID]DeviceConfiguration
DeviceList() []DeviceConfiguration
RemoveDevice(id protocol.DeviceID) (Waiter, error)
SetDevice(DeviceConfiguration) (Waiter, error)
SetDevices([]DeviceConfiguration) (Waiter, error)
IgnoredDevices() []ObservedDevice
IgnoredDevice(id protocol.DeviceID) bool
IgnoredFolder(device protocol.DeviceID, folder string) bool
Subscribe(c Committer)
Subscribe(c Committer) Configuration
Unsubscribe(c Committer)
}
@@ -97,6 +114,7 @@ type wrapper struct {
path string
evLogger events.Logger
myID protocol.DeviceID
queue chan modifyEntry
waiter Waiter // Latest ongoing config change
subs []Committer
@@ -107,12 +125,15 @@ type wrapper struct {
// Wrap wraps an existing Configuration structure and ties it to a file on
// disk.
// The returned Wrapper is a suture.Service, thus needs to be started (added to
// a supervisor).
func Wrap(path string, cfg Configuration, myID protocol.DeviceID, evLogger events.Logger) Wrapper {
w := &wrapper{
cfg: cfg,
path: path,
evLogger: evLogger,
myID: myID,
queue: make(chan modifyEntry, maxModifications),
waiter: noopWaiter{}, // Noop until first config change
mut: sync.NewMutex(),
}
@@ -121,6 +142,8 @@ func Wrap(path string, cfg Configuration, myID protocol.DeviceID, evLogger event
// Load loads an existing file on disk and returns a new configuration
// wrapper.
// The returned Wrapper is a suture.Service, thus needs to be started (added to
// a supervisor).
func Load(path string, myID protocol.DeviceID, evLogger events.Logger) (Wrapper, int, error) {
fd, err := os.Open(path)
if err != nil {
@@ -145,11 +168,13 @@ func (w *wrapper) MyID() protocol.DeviceID {
}
// Subscribe registers the given handler to be called on any future
// configuration changes.
func (w *wrapper) Subscribe(c Committer) {
// configuration changes. It returns the config that is in effect while
// subscribing, that can be used for initial setup.
func (w *wrapper) Subscribe(c Committer) Configuration {
w.mut.Lock()
defer w.mut.Unlock()
w.subs = append(w.subs, c)
w.mut.Unlock()
return w.cfg.Copy()
}
// Unsubscribe de-registers the given handler from any future calls to
@@ -179,11 +204,84 @@ func (w *wrapper) RawCopy() Configuration {
return w.cfg.Copy()
}
// Replace swaps the current configuration object for the given one.
func (w *wrapper) Replace(cfg Configuration) (Waiter, error) {
w.mut.Lock()
defer w.mut.Unlock()
return w.replaceLocked(cfg.Copy())
func (w *wrapper) Modify(fn ModifyFunction) (Waiter, error) {
return w.modifyQueued(fn)
}
func (w *wrapper) modifyQueued(modifyFunc ModifyFunction) (Waiter, error) {
e := modifyEntry{
modifyFunc: modifyFunc,
res: make(chan modifyResult),
}
select {
case w.queue <- e:
default:
return noopWaiter{}, errTooManyModifications
}
res := <-e.res
return res.w, res.err
}
func (w *wrapper) Serve(ctx context.Context) error {
defer w.serveSave()
var e modifyEntry
saveTimer := time.NewTimer(0)
<-saveTimer.C
saveTimerRunning := false
for {
select {
case e = <-w.queue:
case <-saveTimer.C:
w.serveSave()
saveTimerRunning = false
continue
case <-ctx.Done():
return ctx.Err()
}
var waiter Waiter = noopWaiter{}
var err error
// Let the caller modify the config.
to := w.RawCopy()
e.modifyFunc(&to)
// Check if the config was actually changed at all.
w.mut.Lock()
if !reflect.DeepEqual(w.cfg, to) {
waiter, err = w.replaceLocked(to)
if !saveTimerRunning {
saveTimer.Reset(minSaveInterval)
saveTimerRunning = true
}
}
w.mut.Unlock()
e.res <- modifyResult{
w: waiter,
err: err,
}
// Wait for all subscriber to handle the config change before continuing
// to process the next change.
done := make(chan struct{})
go func() {
waiter.Wait()
close(done)
}()
select {
case <-done:
case <-ctx.Done():
return ctx.Err()
}
}
}
func (w *wrapper) serveSave() {
if err := w.Save(); err != nil {
l.Warnln("Failed to save config:", err)
}
}
func (w *wrapper) replaceLocked(to Configuration) (Waiter, error) {
@@ -246,55 +344,16 @@ func (w *wrapper) DeviceList() []DeviceConfiguration {
return w.cfg.Copy().Devices
}
// SetDevices adds new devices to the configuration, or overwrites existing
// devices with the same ID.
func (w *wrapper) SetDevices(devs []DeviceConfiguration) (Waiter, error) {
w.mut.Lock()
defer w.mut.Unlock()
newCfg := w.cfg.Copy()
var replaced bool
for oldIndex := range devs {
replaced = false
for newIndex := range newCfg.Devices {
if newCfg.Devices[newIndex].DeviceID == devs[oldIndex].DeviceID {
newCfg.Devices[newIndex] = devs[oldIndex].Copy()
replaced = true
break
}
}
if !replaced {
newCfg.Devices = append(newCfg.Devices, devs[oldIndex].Copy())
}
}
return w.replaceLocked(newCfg)
}
// SetDevice adds a new device to the configuration, or overwrites an existing
// device with the same ID.
func (w *wrapper) SetDevice(dev DeviceConfiguration) (Waiter, error) {
return w.SetDevices([]DeviceConfiguration{dev})
}
// RemoveDevice removes the device from the configuration
func (w *wrapper) RemoveDevice(id protocol.DeviceID) (Waiter, error) {
w.mut.Lock()
defer w.mut.Unlock()
newCfg := w.cfg.Copy()
for i := range newCfg.Devices {
if newCfg.Devices[i].DeviceID == id {
newCfg.Devices = append(newCfg.Devices[:i], newCfg.Devices[i+1:]...)
return w.replaceLocked(newCfg)
return w.modifyQueued(func(cfg *Configuration) {
if _, i, ok := cfg.Device(id); ok {
cfg.Devices = append(cfg.Devices[:i], cfg.Devices[i+1:]...)
}
}
return noopWaiter{}, nil
})
}
// Folders returns a map of folders. Folder structures should not be changed,
// other than for the purpose of updating via SetFolder().
// Folders returns a map of folders.
func (w *wrapper) Folders() map[string]FolderConfiguration {
w.mut.Lock()
defer w.mut.Unlock()
@@ -312,51 +371,13 @@ func (w *wrapper) FolderList() []FolderConfiguration {
return w.cfg.Copy().Folders
}
// SetFolder adds a new folder to the configuration, or overwrites an existing
// folder with the same ID.
func (w *wrapper) SetFolder(fld FolderConfiguration) (Waiter, error) {
return w.SetFolders([]FolderConfiguration{fld})
}
// SetFolders adds new folders to the configuration, or overwrites existing
// folders with the same ID.
func (w *wrapper) SetFolders(folders []FolderConfiguration) (Waiter, error) {
w.mut.Lock()
defer w.mut.Unlock()
newCfg := w.cfg.Copy()
inds := make(map[string]int, len(w.cfg.Folders))
for i, folder := range newCfg.Folders {
inds[folder.ID] = i
}
filtered := folders[:0]
for _, folder := range folders {
if i, ok := inds[folder.ID]; ok {
newCfg.Folders[i] = folder
} else {
filtered = append(filtered, folder)
}
}
newCfg.Folders = append(newCfg.Folders, filtered...)
return w.replaceLocked(newCfg)
}
// RemoveFolder removes the folder from the configuration
func (w *wrapper) RemoveFolder(id string) (Waiter, error) {
w.mut.Lock()
defer w.mut.Unlock()
newCfg := w.cfg.Copy()
for i := range newCfg.Folders {
if newCfg.Folders[i].ID == id {
newCfg.Folders = append(newCfg.Folders[:i], newCfg.Folders[i+1:]...)
return w.replaceLocked(newCfg)
return w.modifyQueued(func(cfg *Configuration) {
if _, i, ok := cfg.Folder(id); ok {
cfg.Folders = append(cfg.Folders[:i], cfg.Folders[i+1:]...)
}
}
return noopWaiter{}, nil
})
}
// FolderPasswords returns the folder passwords set for this device, for
@@ -374,29 +395,12 @@ func (w *wrapper) Options() OptionsConfiguration {
return w.cfg.Options.Copy()
}
// SetOptions replaces the current options configuration object.
func (w *wrapper) SetOptions(opts OptionsConfiguration) (Waiter, error) {
w.mut.Lock()
defer w.mut.Unlock()
newCfg := w.cfg.Copy()
newCfg.Options = opts.Copy()
return w.replaceLocked(newCfg)
}
func (w *wrapper) LDAP() LDAPConfiguration {
w.mut.Lock()
defer w.mut.Unlock()
return w.cfg.LDAP.Copy()
}
func (w *wrapper) SetLDAP(ldap LDAPConfiguration) (Waiter, error) {
w.mut.Lock()
defer w.mut.Unlock()
newCfg := w.cfg.Copy()
newCfg.LDAP = ldap.Copy()
return w.replaceLocked(newCfg)
}
// GUI returns the current GUI configuration object.
func (w *wrapper) GUI() GUIConfiguration {
w.mut.Lock()
@@ -404,15 +408,6 @@ func (w *wrapper) GUI() GUIConfiguration {
return w.cfg.GUI.Copy()
}
// SetGUI replaces the current GUI configuration object.
func (w *wrapper) SetGUI(gui GUIConfiguration) (Waiter, error) {
w.mut.Lock()
defer w.mut.Unlock()
newCfg := w.cfg.Copy()
newCfg.GUI = gui.Copy()
return w.replaceLocked(newCfg)
}
// IgnoredDevice returns whether or not connection attempts from the given
// device should be silently ignored.
func (w *wrapper) IgnoredDevice(id protocol.DeviceID) bool {
@@ -449,24 +444,22 @@ func (w *wrapper) IgnoredFolder(device protocol.DeviceID, folder string) bool {
func (w *wrapper) Device(id protocol.DeviceID) (DeviceConfiguration, bool) {
w.mut.Lock()
defer w.mut.Unlock()
for _, device := range w.cfg.Devices {
if device.DeviceID == id {
return device.Copy(), true
}
device, _, ok := w.cfg.Device(id)
if !ok {
return DeviceConfiguration{}, false
}
return DeviceConfiguration{}, false
return device.Copy(), ok
}
// Folder returns the configuration for the given folder and an "ok" bool.
func (w *wrapper) Folder(id string) (FolderConfiguration, bool) {
w.mut.Lock()
defer w.mut.Unlock()
for _, folder := range w.cfg.Folders {
if folder.ID == id {
return folder.Copy(), true
}
fcfg, _, ok := w.cfg.Folder(id)
if !ok {
return FolderConfiguration{}, false
}
return FolderConfiguration{}, false
return fcfg.Copy(), ok
}
// Save writes the configuration to disk, and generates a ConfigSaved event.
@@ -502,3 +495,13 @@ func (w *wrapper) RequiresRestart() bool {
func (w *wrapper) setRequiresRestart() {
atomic.StoreUint32(&w.requiresRestart, 1)
}
type modifyEntry struct {
modifyFunc ModifyFunction
res chan modifyResult
}
type modifyResult struct {
w Waiter
err error
}