diff --git a/internal/config/config.go b/internal/config/config.go
index 953d9b670..76133a2aa 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -82,8 +82,6 @@ type FolderConfiguration struct {
IgnoreDelete bool `xml:"ignoreDelete" json:"ignoreDelete"`
Invalid string `xml:"-" json:"invalid"` // Set at runtime when there is an error, not saved
-
- deviceIDs []protocol.DeviceID
}
func (f FolderConfiguration) Copy() FolderConfiguration {
@@ -144,12 +142,11 @@ func (f *FolderConfiguration) HasMarker() bool {
}
func (f *FolderConfiguration) DeviceIDs() []protocol.DeviceID {
- if f.deviceIDs == nil {
- for _, n := range f.Devices {
- f.deviceIDs = append(f.deviceIDs, n.DeviceID)
- }
+ deviceIDs := make([]protocol.DeviceID, len(f.Devices))
+ for i, n := range f.Devices {
+ deviceIDs[i] = n.DeviceID
}
- return f.deviceIDs
+ return deviceIDs
}
type VersioningConfiguration struct {
diff --git a/internal/model/model.go b/internal/model/model.go
index 21e20c226..a4ca60f0a 100644
--- a/internal/model/model.go
+++ b/internal/model/model.go
@@ -691,14 +691,7 @@ func (m *Model) Close(device protocol.DeviceID, err error) {
conn, ok := m.rawConn[device]
if ok {
- if conn, ok := conn.(*tls.Conn); ok {
- // If the underlying connection is a *tls.Conn, Close() does more
- // than it says on the tin. Specifically, it sends a TLS alert
- // message, which might block forever if the connection is dead
- // and we don't have a deadline site.
- conn.SetWriteDeadline(time.Now().Add(250 * time.Millisecond))
- }
- conn.Close()
+ closeRawConn(conn)
}
delete(m.protoConn, device)
delete(m.rawConn, device)
@@ -1732,30 +1725,132 @@ func (m *Model) VerifyConfiguration(from, to config.Configuration) error {
func (m *Model) CommitConfiguration(from, to config.Configuration) bool {
// TODO: This should not use reflect, and should take more care to try to handle stuff without restart.
- // Adding, removing or changing folders requires restart
- if !reflect.DeepEqual(from.Folders, to.Folders) {
- return false
+ // Go through the folder configs and figure out if we need to restart or not.
+
+ fromFolders := mapFolders(from.Folders)
+ toFolders := mapFolders(to.Folders)
+ for folderID := range toFolders {
+ if _, ok := fromFolders[folderID]; !ok {
+ // A folder was added. Requires restart.
+ if debug {
+ l.Debugln(m, "requires restart, adding folder", folderID)
+ }
+ return false
+ }
+ }
+
+ for folderID, fromCfg := range fromFolders {
+ toCfg, ok := toFolders[folderID]
+ if !ok {
+ // A folder was removed. Requires restart.
+ if debug {
+ l.Debugln(m, "requires restart, removing folder", folderID)
+ }
+ return false
+ }
+
+ // This folder exists on both sides. Compare the device lists, as we
+ // can handle adding a device (but not currently removing one).
+
+ fromDevs := mapDevices(fromCfg.DeviceIDs())
+ toDevs := mapDevices(toCfg.DeviceIDs())
+ for dev := range fromDevs {
+ if _, ok := toDevs[dev]; !ok {
+ // A device was removed. Requires restart.
+ if debug {
+ l.Debugln(m, "requires restart, removing device", dev, "from folder", folderID)
+ }
+ return false
+ }
+ }
+
+ for dev := range toDevs {
+ if _, ok := fromDevs[dev]; !ok {
+ // A device was added. Handle it!
+
+ m.fmut.Lock()
+ m.pmut.Lock()
+
+ m.folderCfgs[folderID] = toCfg
+ m.folderDevices[folderID] = append(m.folderDevices[folderID], dev)
+ m.deviceFolders[dev] = append(m.deviceFolders[dev], folderID)
+
+ // If we already have a connection to this device, we should
+ // disconnect it so that we start sharing the folder with it.
+ // We close the underlying connection and let the normal error
+ // handling kick in to clean up and reconnect.
+ if conn, ok := m.rawConn[dev]; ok {
+ closeRawConn(conn)
+ }
+
+ m.pmut.Unlock()
+ m.fmut.Unlock()
+ }
+ }
+
+ // Check if anything else differs, apart from the device list.
+ fromCfg.Devices = nil
+ toCfg.Devices = nil
+ if !reflect.DeepEqual(fromCfg, toCfg) {
+ if debug {
+ l.Debugln(m, "requires restart, folder", folderID, "configuration differs")
+ }
+ return false
+ }
}
// Removing a device requres restart
- toDevs := make(map[protocol.DeviceID]bool, len(from.Devices))
- for _, dev := range to.Devices {
- toDevs[dev.DeviceID] = true
- }
+ toDevs := mapDeviceCfgs(from.Devices)
for _, dev := range from.Devices {
if _, ok := toDevs[dev.DeviceID]; !ok {
+ if debug {
+ l.Debugln(m, "requires restart, device", dev.DeviceID, "was removed")
+ }
return false
}
}
// All of the generic options require restart
if !reflect.DeepEqual(from.Options, to.Options) {
+ if debug {
+ l.Debugln(m, "requires restart, options differ")
+ }
return false
}
return true
}
+// mapFolders returns a map of folder ID to folder configuration for the given
+// slice of folder configurations.
+func mapFolders(folders []config.FolderConfiguration) map[string]config.FolderConfiguration {
+ m := make(map[string]config.FolderConfiguration, len(folders))
+ for _, cfg := range folders {
+ m[cfg.ID] = cfg
+ }
+ return m
+}
+
+// mapDevices returns a map of device ID to nothing for the given slice of
+// device IDs.
+func mapDevices(devices []protocol.DeviceID) map[protocol.DeviceID]struct{} {
+ m := make(map[protocol.DeviceID]struct{}, len(devices))
+ for _, dev := range devices {
+ m[dev] = struct{}{}
+ }
+ return m
+}
+
+// mapDeviceCfgs returns a map of device ID to nothing for the given slice of
+// device configurations.
+func mapDeviceCfgs(devices []config.DeviceConfiguration) map[protocol.DeviceID]struct{} {
+ m := make(map[protocol.DeviceID]struct{}, len(devices))
+ for _, dev := range devices {
+ m[dev.DeviceID] = struct{}{}
+ }
+ return m
+}
+
func filterIndex(folder string, fs []protocol.FileInfo, dropDeletes bool) []protocol.FileInfo {
for i := 0; i < len(fs); {
if fs[i].Flags&^protocol.FlagsAll != 0 {
@@ -1816,3 +1911,14 @@ func getChunk(data []string, skip, get int) ([]string, int, int) {
}
return data[skip : skip+get], 0, 0
}
+
+func closeRawConn(conn io.Closer) error {
+ if conn, ok := conn.(*tls.Conn); ok {
+ // If the underlying connection is a *tls.Conn, Close() does more
+ // than it says on the tin. Specifically, it sends a TLS alert
+ // message, which might block forever if the connection is dead
+ // and we don't have a deadline set.
+ conn.SetWriteDeadline(time.Now().Add(250 * time.Millisecond))
+ }
+ return conn.Close()
+}
diff --git a/internal/rc/rc.go b/internal/rc/rc.go
index 634f96de0..793b6f032 100644
--- a/internal/rc/rc.go
+++ b/internal/rc/rc.go
@@ -63,6 +63,10 @@ func NewProcess(addr string) *Process {
return p
}
+func (p *Process) ID() protocol.DeviceID {
+ return p.id
+}
+
// LogTo creates the specified log file and ensures that stdout and stderr
// from the Start()ed process is redirected there. Must be called before
// Start().
@@ -229,6 +233,34 @@ func (p *Process) RescanDelay(folder string, delaySeconds int) error {
return err
}
+func (p *Process) ConfigInSync() (bool, error) {
+ bs, err := p.Get("/rest/system/config/insync")
+ if err != nil {
+ return false, err
+ }
+ return bytes.Contains(bs, []byte("true")), nil
+}
+
+func (p *Process) GetConfig() (config.Configuration, error) {
+ var cfg config.Configuration
+ bs, err := p.Get("/rest/system/config")
+ if err != nil {
+ return cfg, err
+ }
+
+ err = json.Unmarshal(bs, &cfg)
+ return cfg, err
+}
+
+func (p *Process) PostConfig(cfg config.Configuration) error {
+ buf := new(bytes.Buffer)
+ if err := json.NewEncoder(buf).Encode(cfg); err != nil {
+ return err
+ }
+ _, err := p.Post("/rest/system/config", buf)
+ return err
+}
+
func InSync(folder string, ps ...*Process) bool {
for _, p := range ps {
p.eventMut.Lock()
diff --git a/test/.gitignore b/test/.gitignore
index 8127f40cc..85a787119 100644
--- a/test/.gitignore
+++ b/test/.gitignore
@@ -1,6 +1,7 @@
s1
s2
s3
+s4
s12-1
s12-2
s23-2
@@ -20,3 +21,4 @@ h*/index*
panic-*.log
audit-*.log
h*/config.xml.v*
+h*/config.xml.orig
diff --git a/test/h1/config.xml b/test/h1/config.xml
index 732b2f735..e0ae9d398 100644
--- a/test/h1/config.xml
+++ b/test/h1/config.xml
@@ -3,6 +3,7 @@
+
false
1
@@ -31,6 +32,9 @@
127.0.0.1:22003
+
+ 127.0.0.1:22004
+
127.0.0.1:8081
testuser
diff --git a/test/h4/cert.pem b/test/h4/cert.pem
new file mode 100644
index 000000000..d329435cb
--- /dev/null
+++ b/test/h4/cert.pem
@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIID6TCCAlGgAwIBAgIISz5XufRr9xMwDQYJKoZIhvcNAQELBQAwFDESMBAGA1UE
+AxMJc3luY3RoaW5nMB4XDTE1MDcyMjA3MDIzOFoXDTQ5MTIzMTIzNTk1OVowFDES
+MBAGA1UEAxMJc3luY3RoaW5nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKC
+AYEA4nE2FPVQkfMStJms0SUEjSi5qUC4I2+aCFD+q6rLJHhgzdvjXoQ8iWX8hFLu
+nza3mMKTSjcThnpR/yA1S0ipATsdQ5c5xjceliSLDxImBcBaMtvGejgOlFwC6zTz
+5CJAnLo8odQtAgaaUtGJU145OAHM/cTA0xKd+nh0UvuJHT56Ur6dZ/VKzONnWsUW
+qI/YVp7mRvv1PimN74ppTQSadU1s3gyq3b7mnl/aWjN42/G6kO27NXA1lVblnFk/
+Cee6HFxUIy5upTFXnAm1DaEFVdzQ1dxBAEXwIbh2WOXeVCyDONzaqVcYPYQKG5NT
+KbYY08rnDmRFlURHFQ/eEr49zniLrQRfL3pSNCEGmuVpPAEsuGQ5EQW1b8aEFMgp
+IR+Jo59JyU04HrP27VctyUEBT4MCQn4G9gN6Qy1EKTKq49UVNR+1eMtuq9/o6tXl
+rwepnO9AITclPdpvGc93hTshEBZFQF+rHkUMoj7jXr9zAGchRoY8cxaJM0DGrpjc
+uGONAgMBAAGjPzA9MA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcD
+AQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAYEAgiC2
+LYPXPCtuaF7qGbas0A5zYtPr0PrXaILl4uYA63+ZXKPMOQ+LkdgRzSQxvKLrPLQM
+/LwWOTONuqT2sw8Wj+MilzDOXIlEWG2Gqy3/xS7H5RAkZqjVHhuBRXnJiZEl5HAh
+ASMGiyejII2uN7k+5sjCFmuSfdcI18f/AjUL5fz53TpIJinyCakQipdicI9jZvLR
+jJ2sqy9wJ3yhTtUm5M33bsLPjhnwMkTTYvvMomfRI8qUYflWxb5BZ82FvNVUE9kA
+hDdJzluINMofMAblyf9TxX0q1bunPc9soAMtUSDWRmNtviV9uggEdtGYrmDrK7Dz
++89AB60QSN6MJzVNPdJZCPvefuJjk9isQBUbQE/CsVFeooKJ/DU5arbUV2mjaifV
+Z6GxHiEkynSWaNMQLioi+vPguMdAuotdqpInVjCLKJbKiOXrYfIhYJFATc0lRBHx
+9LUH020HOACgX+WVFiDEDx7OCu868IbDJK/gryb5IfIpbaY4xit9eoqMS4BP
+-----END CERTIFICATE-----
diff --git a/test/h4/config.xml b/test/h4/config.xml
new file mode 100644
index 000000000..2921c5926
--- /dev/null
+++ b/test/h4/config.xml
@@ -0,0 +1,47 @@
+
+
+
+
+ 1
+ 16
+ 0
+ random
+ false
+
+
+ dynamic
+
+
+ 127.0.0.1:8084
+ PMA5yUTG-Mw98nJ0YEtWTCHlM5O4aNi0
+
+
+ 127.0.0.1:22004
+ udp4://announce.syncthing.net:22026
+ udp6://announce-v6.syncthing.net:22026
+ false
+ false
+ 21025
+ [ff32::5222]:21026
+ 0
+ 0
+ 60
+ false
+ false
+ 60
+ 30
+ 10
+ -1
+
+ true
+ 12
+ 24
+ true
+ 5
+ true
+ false
+ 0
+ 30
+ 60
+
+
diff --git a/test/h4/https-cert.pem b/test/h4/https-cert.pem
new file mode 100644
index 000000000..e767708eb
--- /dev/null
+++ b/test/h4/https-cert.pem
@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIID3zCCAkegAwIBAgIIWH6f9/hiHaowDQYJKoZIhvcNAQELBQAwDzENMAsGA1UE
+AxMEc3lubzAeFw0xNTA3MjIwNzE1MjlaFw00OTEyMzEyMzU5NTlaMA8xDTALBgNV
+BAMTBHN5bm8wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCgideynuoI
+MfN2PR7WfPWvRjnYNuNp5U1C5GzAfrKxVaHkfpt+AsXHHsuo1Xl3gdsIs1Uc2Z8R
+yLPxFgT+bLKKqwTw4D/9JTHtF2vOLkZLB4/0Bhe2BAXepEEIZDqEHsNE7A8ma9Jv
+JlBxW55xoXUE5ak2tNvxQneoDj+WKpd24jyZBMp/TC52dhy6TmDfahrQjU29Nz7n
+tlVC1eol7YqB7+M1CXK2OK74m9J9G8tnweDKJKPv9t011dIhyd2GqRI36fU1EuIC
++NSWhcl1VGEa3eCN9Bn+pUo5oDSiMfGmbVo77al31wpN+2+BprH/JTWSWtvBG6uh
+Cyq5cqkDxMeXmCD863+xorE0hyqZkRrS2XSaJI7hhOgVCUUrfPMK3p9n1pkZ+RfN
+AtYFPhit2bJyjSBJNN0qxnmMHspZFO+eoeNQkaeL7sDeHLo2ZEUIJMyq4ElsimLU
+i/+bQCaHl4vz/rz8nRNnIsm4o2adgLie3ZA2lJ+5vEBN+1GlaHIrEnUCAwEAAaM/
+MD0wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD
+AjAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBgQBsbVHPEvzX2Emas+yG
+zbKa1wcuxNWn7nmYjz8YXuFURjGAt1U8wPV+YhgZrhR1rImwGRkXjRwL3vCvm5xi
+aTNNK2g132amMKhWcAwm/bXJsW3smFpUmmb6j1jZj2eQo3UFNpEql+GzHF/iLWgA
+74xsqRkqTR/tkoD/W47ASn92rlj8vKmVafiq132/YlqxzaJB4FQyfmdHd1HMsStk
+r531DXSBsK9CBnM/oEkoCBsJFi6xiUNf7D7wjvoVnCcrIx4bNXiMKgbZA/M0mh9t
+bDI5b+2j1Af7npPzHAEYEWbWSGwpDBnpB9PuG11WjozLpwDA2My5yjiwHQYw3cIV
+QM17Oia97QjgOLbbG5Hpy6SF0KxUyCINpg780U7WKyVLherpdQ1ABRmlC0laXDh5
+Oq500d316ej28VITWj3gMhocw4KwXpkjh9cweLTPV7wiUsoO2ksEMjEPdGCjzHXg
+k7KQB7dqbOS7VIOJj8+GPbaf3aTdG+b1z3KVcDMH+59TddE=
+-----END CERTIFICATE-----
diff --git a/test/h4/https-key.pem b/test/h4/https-key.pem
new file mode 100644
index 000000000..977d43a0c
--- /dev/null
+++ b/test/h4/https-key.pem
@@ -0,0 +1,39 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIG4wIBAAKCAYEAoInXsp7qCDHzdj0e1nz1r0Y52DbjaeVNQuRswH6ysVWh5H6b
+fgLFxx7LqNV5d4HbCLNVHNmfEciz8RYE/myyiqsE8OA//SUx7Rdrzi5GSweP9AYX
+tgQF3qRBCGQ6hB7DROwPJmvSbyZQcVuecaF1BOWpNrTb8UJ3qA4/liqXduI8mQTK
+f0wudnYcuk5g32oa0I1NvTc+57ZVQtXqJe2Kge/jNQlytjiu+JvSfRvLZ8HgyiSj
+7/bdNdXSIcndhqkSN+n1NRLiAvjUloXJdVRhGt3gjfQZ/qVKOaA0ojHxpm1aO+2p
+d9cKTftvgaax/yU1klrbwRuroQsquXKpA8THl5gg/Ot/saKxNIcqmZEa0tl0miSO
+4YToFQlFK3zzCt6fZ9aZGfkXzQLWBT4Yrdmyco0gSTTdKsZ5jB7KWRTvnqHjUJGn
+i+7A3hy6NmRFCCTMquBJbIpi1Iv/m0Amh5eL8/68/J0TZyLJuKNmnYC4nt2QNpSf
+ubxATftRpWhyKxJ1AgMBAAECggGAVdnBHsV69Az6XIXNAvjqTeQpNOYNcWjti1Mq
+kTpwBwN7Qv0t3BJRf+2JDe2zOmSYJKv6XSZHubPx/oA/BWxNgnh4ePQDZDXK4DaB
+MU5vytntcpr7fRvjo6+FE5696D+nPylZ5LsOWuBLboOHVM76DDdg6V+IqxlXcejE
+umJmg23y6AW24KJ1ymXZcQxPI8rTMioOo5xyqGlKaSaKQ+QnCNunToqR7L6dW1fB
+FaSSfxcgRhmYDdCfdZW1/Nm9/LBWs/qnmuUwD35jAaVDJ0WiwZcz0UeqrcWtsCiP
+lNJJN6EuIjcLupr3HzQXqI2sBZ9eoItoVGXr5JTi93mo1r5re4sXSZtM3YW4imhD
+11XTpmspsUvat4tSWz+Bpq0i1dI68aTBOf5P3WNONtW8Q31egGevHzfjyD0ODG/d
+Gr8BFsDJNA8QhuI5q1M3rBelo8/GtLQ4sQd5KaCFxC2I+qy0a4cV3NFxvI4Y+QnE
+E0osBkSRmFAgyHN5qmPhi6cgctqVAoHBAMp05vSrp8lTcW0bu3eWiCnAEeOgXmV6
+BWuxJexPmfgV7uAaYGbO6/lAyLtTYV5EshV2QAPLB9F93uTVIM8MRJTqhG+lWwde
+gmlLq43/cVn7RNWEFuw8KbzxOippGi+IAD9Pg8fHqOfTpVH6t+jKY45KOXTfQ5tZ
+SL8Y/35CaQUDYSWM/zj0uRYnXMgkjDE8bt8dJv0Ozajd+zL+VK7BTb2BHq2lOplG
+kqrecaflg7ooXrwLKWuMBbnbl2nHZ7MWgwKBwQDK/uqFd/R7/yQfuCI4fjfJ8V0m
+do+UDaNxQpYHyku/AeSaUjVasxirNIrStEF6DuNPAYrUwNPErVveVyEUEV57JY9A
+qutts10gD4sdd6afIVBdVmiM0pKK1PHeQFecl6mY6qMPGPi2BKFEvVF3Gg938R/M
+OfAS0/SJDD0BMwTlcMhjGZo78o3K1Hcy1tqGPcYbkG5mdAVq9BQxxVQP+S/bnKyW
+5KHPCZYr9BAHhbjLXxxrtB6cZyDgCQ4KZFjloacCgcEAt2C1xQ4qNvgGuB4zanmF
+sdNQIM6kUeP5PvdA80+SlZxANuqNQPHR2X2tk8dNXVZ5u2jVSNpApacOGlVVl1R0
+VjIpbProfb9D/l3U8RRbtnYafg9bt/Qylfolhj6WwlC8cJv0MCOPwRP6HUwsAoY3
+MK3YZxzHHtH7S2Q4H0PF3g2Wk62niw5XC1Lx/jLkbMBhaGP+aZ5b98XA/wpQ580d
+PjXS9NPBRQ4gUPaVGc+QxjBExqyRguFcWmElP2GncxZDAoHATJF6xH1KqrrCVXSO
+8+AoCvQPvsJZxe6fB8ml7apQh+ue3tbDaULEu09GTdPQHsoe014xj65sMnNxg5w5
+zef/S1QPhMTzqJ1PMxip0KOhJcTbG1nMddG3lMZdtQdwBJDwV82pU7iHl6CHc/Y1
+FEewLf21kMMJ2xA33LnRCPLFlgXEkBzIIHSNJ0Sc8YA5TQlgAGWqPtrkcENAmsVj
+v+KuOpgOQZxbrExhaJLWuP+nhI6LmdSG91eu/tJriV/waC1hAoHAOODRfnJzvj01
+/QVvtqTCcB1mAFX7myQTImjcqW2PhK7+0cKpSOW/LlhMNxHiTM+S/7M26NfzkMeE
++9yltJkRMGSgsRNsylbKdqHVM7wxDIS5fwQh2jJlPhpIXZIsFPZFA6xI94yyFND9
+HYnawbkiHGHh1CTSRIQDhbdemTj97qhtOsb6txCypvkyYGtb6WQ+MN2an11TdM/9
+Vj1iRoOjyLJ46px8Ufsv7PDY4A9gukqgrTI6FApeLb/qn4iYfVrK
+-----END RSA PRIVATE KEY-----
diff --git a/test/h4/key.pem b/test/h4/key.pem
new file mode 100644
index 000000000..9b2be1337
--- /dev/null
+++ b/test/h4/key.pem
@@ -0,0 +1,39 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
diff --git a/test/norestart_test.go b/test/norestart_test.go
new file mode 100644
index 000000000..67ac27a50
--- /dev/null
+++ b/test/norestart_test.go
@@ -0,0 +1,85 @@
+// Copyright (C) 2014 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// +build integration
+
+package integration
+
+import (
+ "log"
+ "os"
+ "testing"
+
+ "github.com/syncthing/protocol"
+ "github.com/syncthing/syncthing/internal/config"
+ "github.com/syncthing/syncthing/internal/rc"
+)
+
+func TestAddDeviceWithoutRestart(t *testing.T) {
+ log.Println("Cleaning...")
+ err := removeAll("s1", "h1/index*", "s4", "h4/index*")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ log.Println("Generating files...")
+ err = generateFiles("s1", 100, 18, "../LICENSE")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ p1 := startInstance(t, 1)
+ defer checkedStop(t, p1)
+
+ p4 := startInstance(t, 4)
+ defer checkedStop(t, p4)
+
+ if ok, err := p1.ConfigInSync(); err != nil || !ok {
+ t.Fatal("p1 should be in sync;", ok, err)
+ }
+ if ok, err := p4.ConfigInSync(); err != nil || !ok {
+ t.Fatal("p4 should be in sync;", ok, err)
+ }
+
+ // Add the p1 device to p4. Back up and restore p4's config first.
+
+ log.Println("Adding p1 to p4...")
+
+ os.Remove("h4/config.xml.orig")
+ os.Rename("h4/config.xml", "h4/config.xml.orig")
+ defer os.Rename("h4/config.xml.orig", "h4/config.xml")
+
+ cfg, err := p4.GetConfig()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ devCfg := config.DeviceConfiguration{
+ DeviceID: p1.ID(),
+ Name: "s1",
+ Addresses: []string{"127.0.0.1:22001"},
+ Compression: protocol.CompressMetadata,
+ }
+ cfg.Devices = append(cfg.Devices, devCfg)
+
+ cfg.Folders[0].Devices = append(cfg.Folders[0].Devices, config.FolderDeviceConfiguration{DeviceID: p1.ID()})
+
+ if err = p4.PostConfig(cfg); err != nil {
+ t.Fatal(err)
+ }
+
+ // The change should not require a restart, so the config should be "in sync"
+
+ if ok, err := p4.ConfigInSync(); err != nil || !ok {
+ t.Fatal("p4 should be in sync;", ok, err)
+ }
+
+ // Wait for the devices to connect and sync.
+
+ log.Println("Waiting for p1 and p4 to connect and sync...")
+
+ rc.AwaitSync("default", p1, p4)
+}