Quellcode durchsuchen

Add devices without restart (fixes #2083)

Jakob Borg vor 10 Jahren
Ursprung
Commit
76480adda5

+ 4 - 7
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 {

+ 121 - 15
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
+		}
 	}
 
-	// Removing a device requres restart
-	toDevs := make(map[protocol.DeviceID]bool, len(from.Devices))
-	for _, dev := range to.Devices {
-		toDevs[dev.DeviceID] = true
+	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 := 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()
+}

+ 32 - 0
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()

+ 2 - 0
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

+ 4 - 0
test/h1/config.xml

@@ -3,6 +3,7 @@
         <device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU"></device>
         <device id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU"></device>
         <device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU"></device>
+        <device id="7PBCTLL-JJRYBSA-MOWZRKL-MSDMN4N-4US4OMX-SYEXUS4-HSBGNRY-CZXRXAT"></device>
         <versioning></versioning>
         <lenientMtimes>false</lenientMtimes>
         <copiers>1</copiers>
@@ -31,6 +32,9 @@
     <device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU" name="s3" compression="metadata" introducer="false">
         <address>127.0.0.1:22003</address>
     </device>
+    <device id="7PBCTLL-JJRYBSA-MOWZRKL-MSDMN4N-4US4OMX-SYEXUS4-HSBGNRY-CZXRXAT" name="s4" compression="metadata" introducer="false">
+        <address>127.0.0.1:22004</address>
+    </device>
     <gui enabled="true" tls="false">
         <address>127.0.0.1:8081</address>
         <user>testuser</user>

+ 23 - 0
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-----

+ 47 - 0
test/h4/config.xml

@@ -0,0 +1,47 @@
+<configuration version="10">
+    <folder id="default" path="s4" ro="false" rescanIntervalS="60" ignorePerms="false" autoNormalize="false">
+        <device id="7PBCTLL-JJRYBSA-MOWZRKL-MSDMN4N-4US4OMX-SYEXUS4-HSBGNRY-CZXRXAT"></device>
+        <versioning></versioning>
+        <copiers>1</copiers>
+        <pullers>16</pullers>
+        <hashers>0</hashers>
+        <order>random</order>
+        <ignoreDelete>false</ignoreDelete>
+    </folder>
+    <device id="7PBCTLL-JJRYBSA-MOWZRKL-MSDMN4N-4US4OMX-SYEXUS4-HSBGNRY-CZXRXAT" name="s4" compression="metadata" introducer="false">
+        <address>dynamic</address>
+    </device>
+    <gui enabled="true" tls="false">
+        <address>127.0.0.1:8084</address>
+        <apikey>PMA5yUTG-Mw98nJ0YEtWTCHlM5O4aNi0</apikey>
+    </gui>
+    <options>
+        <listenAddress>127.0.0.1:22004</listenAddress>
+        <globalAnnounceServer>udp4://announce.syncthing.net:22026</globalAnnounceServer>
+        <globalAnnounceServer>udp6://announce-v6.syncthing.net:22026</globalAnnounceServer>
+        <globalAnnounceEnabled>false</globalAnnounceEnabled>
+        <localAnnounceEnabled>false</localAnnounceEnabled>
+        <localAnnouncePort>21025</localAnnouncePort>
+        <localAnnounceMCAddr>[ff32::5222]:21026</localAnnounceMCAddr>
+        <maxSendKbps>0</maxSendKbps>
+        <maxRecvKbps>0</maxRecvKbps>
+        <reconnectionIntervalS>60</reconnectionIntervalS>
+        <startBrowser>false</startBrowser>
+        <upnpEnabled>false</upnpEnabled>
+        <upnpLeaseMinutes>60</upnpLeaseMinutes>
+        <upnpRenewalMinutes>30</upnpRenewalMinutes>
+        <upnpTimeoutSeconds>10</upnpTimeoutSeconds>
+        <urAccepted>-1</urAccepted>
+        <urUniqueID></urUniqueID>
+        <restartOnWakeup>true</restartOnWakeup>
+        <autoUpgradeIntervalH>12</autoUpgradeIntervalH>
+        <keepTemporariesH>24</keepTemporariesH>
+        <cacheIgnoredFiles>true</cacheIgnoredFiles>
+        <progressUpdateIntervalS>5</progressUpdateIntervalS>
+        <symlinksEnabled>true</symlinksEnabled>
+        <limitBandwidthInLan>false</limitBandwidthInLan>
+        <databaseBlockCacheMiB>0</databaseBlockCacheMiB>
+        <pingTimeoutS>30</pingTimeoutS>
+        <pingIdleTimeS>60</pingIdleTimeS>
+    </options>
+</configuration>

+ 23 - 0
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-----

+ 39 - 0
test/h4/https-key.pem

@@ -0,0 +1,39 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----

+ 39 - 0
test/h4/key.pem

@@ -0,0 +1,39 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIG4wIBAAKCAYEA4nE2FPVQkfMStJms0SUEjSi5qUC4I2+aCFD+q6rLJHhgzdvj
+XoQ8iWX8hFLunza3mMKTSjcThnpR/yA1S0ipATsdQ5c5xjceliSLDxImBcBaMtvG
+ejgOlFwC6zTz5CJAnLo8odQtAgaaUtGJU145OAHM/cTA0xKd+nh0UvuJHT56Ur6d
+Z/VKzONnWsUWqI/YVp7mRvv1PimN74ppTQSadU1s3gyq3b7mnl/aWjN42/G6kO27
+NXA1lVblnFk/Cee6HFxUIy5upTFXnAm1DaEFVdzQ1dxBAEXwIbh2WOXeVCyDONza
+qVcYPYQKG5NTKbYY08rnDmRFlURHFQ/eEr49zniLrQRfL3pSNCEGmuVpPAEsuGQ5
+EQW1b8aEFMgpIR+Jo59JyU04HrP27VctyUEBT4MCQn4G9gN6Qy1EKTKq49UVNR+1
+eMtuq9/o6tXlrwepnO9AITclPdpvGc93hTshEBZFQF+rHkUMoj7jXr9zAGchRoY8
+cxaJM0DGrpjcuGONAgMBAAECggGAf0LhAiZcgan6eUVkuqXzSOH6dgTJeCDgkIv0
+lMYIJRcCUK+juRrYat/GaxewxAocZN31qWAKuSlFq/yN9yF+2hI/AB2deqi/p+Ih
+xPaOJ+1SxAKAKXAXwYl0mnvIFg6qAWspaEm2gcz0LldUtmXeAnwAmR5awEVWQ84u
+kfSLusPCO36lOCfDQiMLkxfxBArTqtri0EIKMkVoX5eKVp6fsA0zghfcb4M6WQfF
+z6vd4L6Z+5mf/QhzFNshcB04MHjqMNRY5WQATZiT1KW3z1kzUWe5eWWSxYQHOVEu
+VOZuMpsq2TuwBEJDEzzJoOVXRzx6AfSNdyrGYEkq05h/vxwQZTIdZKs97s1nwzu2
+pkltY1Pf3BdjAvfpCAkxmdu8l+fjlMmgav/lT2O4ZHTbu1MaqNLN5QLGiLxS0I6f
+gdS9iNgYMVwfpGi7UVNLqrG2nxQmYB0LQyZqNFa+9wNbUzN3h3Qq6eKTXl0uBP5C
+PdMUdJ3pF7iJM8tshcTb9ALBU/wBAoHBAOVpjYyXtNQOBm/aFkTcJB81AeBgCk3f
+lxWAs+GwprlPnwCZdH5CvYD2ULGChwaGoYXItRFbWUyP6Tl9c7g3c/MRZJ1M78P9
+VXA30KPKm9T2dT5ZDCJSUnkZPYP0EagfpYJ9dRxK/55uZ+IGtbXcjkPAVe3wzZfZ
+8aWgg9w/qmqgiJ8PUmsYY6lILokj2uanmYo410e3RPvN1+qei78RBW6XAh8yJJNL
+R7vjpjcxtWvY/PKt8b2Yo+DpnHYWm/AqyQKBwQD8r4npkjzrbrS5BQ1mkNDnSaax
+p7j0hWW+bN0c/EHial2ESKw6vnN/Y+6WcdMq1SgiwkFA7OpthHhY/bUkuyu2BQyo
+dLFL05KOusS98YTOdMlho4lGJS4HdCi9qeYuroS8gjYsOmDf2PdM1bHKjzkGG0JL
+igewb6AaF6Yp46jbzqz+PE6WbTTdkfdWXtmDIRTTTOfY7ovG0xLAaYJqNEOzbnQT
+Emj0ggYNaokGfO6uOk6okuRP7VLaVnxXbvoeUKUCgcBIbORlKFfMQolBsqYpIx68
+Q23OOkPGhfoarcEcVTqtcjeOZuPiIIvXNOwQvlaGduZzaAPR8PbmNuC4Z6Sq2cbf
+S/RpvKpNQ6M/hD94FjTQLOaiwlYUV8z1skQ7bkhMvYDxC053mi3NBKoDL38aZQD8
+3rHCJq2hbQre8Sfv1qGke/3lyV6JtO9xt/oJDarD+tF8U6mTWIaMwFWUGm2f6m2+
+linzU088uR1ycdI9xpGx9JUWwFd7Nb82+EmO9mBQmBECgcEAqHubJ2RcvlZ4pg1a
+XBMfV7hiL3638kKoDoqj/FmuzHtDk5qpTBoFBOHrCeEnfh3WvyZrQBE4VoHHhP7V
+s4IhqSJAyGnWdcrCo+yglk3d0ZNJW5MhSuYrhMjNCXmpg2LWGqNv35mlUlxmuJKc
+E4Xf7dRrJdcJPXmQdRVjs/aadsWdz38Cn4Z9g2d6Vdq0iZybODDFPn4AMTg3/pfb
+X1kt8wwo1TanSLERvAxXBT50HzO9kuUu2qRRZEfabKoQl/oJAoHAehR8ULlvRKFi
+ZAW/uzKT3CLEa0z8JDdQTEsfxAfaeJ/EjMHgxdni5b45c80MBJbmJyetiMg5tJxM
+wGKmmux/PuDjg5YEdvJLjIZvBrGlZvLlSw9US13zn+RKglKveGBOxc4qx56AJn1Z
+GLcpjdNq4kmbXq5DtSig+jpqnfAU9bKF9duOSxKoYQv9NapndI900ozW98+1SWiC
+bxvuPS5n7boLfwLlmvIhX7L/V7iLc5rCAI+0b08JsmMmIDOlytJW
+-----END RSA PRIVATE KEY-----

+ 85 - 0
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)
+}