From 2c866277a293f778f54290d7ddc246203f8eef54 Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Thu, 16 May 2019 23:11:46 +0200 Subject: [PATCH] lib/api, lib/connections, gui: Show connection error for disconnected devices (fixes #3345) (#5727) * lib/api, lib/connections, gui: Show connection error for disconnected devices (fixes #3345) This adds functionality in the connetions service to track the last error per address. That is in turn exposed in the /rest/system/status API method, as that is also where we already show the listener status from the connection service. The GUI uses this info where it lists addresses, showing errors (if any) in red underneath each address. I also slightly refactored the existing status method on the connection service to have a better name and return typed information. * ok * review * formatting * review --- gui/default/index.html | 10 ++- .../syncthing/core/syncthingController.js | 10 +++ lib/api/api.go | 3 +- lib/api/mocked_connections_test.go | 10 ++- lib/connections/service.go | 69 ++++++++++++++++--- lib/connections/structs.go | 1 + 6 files changed, 88 insertions(+), 15 deletions(-) diff --git a/gui/default/index.html b/gui/default/index.html index 1868052c..bca5b8c7 100644 --- a/gui/default/index.html +++ b/gui/default/index.html @@ -715,8 +715,14 @@ - {{addr}}
- {{addr}}
+ + {{addr}}
+ {{abbreviatedError(addr)}}
+
+ + {{addr}}
+ {{abbreviatedError(addr)}}
+
diff --git a/gui/default/syncthing/core/syncthingController.js b/gui/default/syncthing/core/syncthingController.js index b47f1777..44ea33a7 100755 --- a/gui/default/syncthing/core/syncthingController.js +++ b/gui/default/syncthing/core/syncthingController.js @@ -2406,4 +2406,14 @@ angular.module('syncthing.core') $scope.saveConfig(); } }; + + $scope.abbreviatedError = function (addr) { + var status = $scope.system.lastDialStatus[addr]; + if (!status || !status.error) { + return null; + } + var time = $filter('date')(status.when, "HH:mm:ss") + var err = status.error.replace(/.+: /, ''); + return err + " (" + time + ")"; + } }); diff --git a/lib/api/api.go b/lib/api/api.go index 5146f09a..eb4c415e 100644 --- a/lib/api/api.go +++ b/lib/api/api.go @@ -898,7 +898,8 @@ func (s *service) getSystemStatus(w http.ResponseWriter, r *http.Request) { res["discoveryErrors"] = discoErrors } - res["connectionServiceStatus"] = s.connectionsService.Status() + res["connectionServiceStatus"] = s.connectionsService.ListenerStatus() + res["lastDialStatus"] = s.connectionsService.ConnectionStatus() // cpuUsage.Rate() is in milliseconds per second, so dividing by ten // gives us percent res["cpuPercent"] = s.cpu.Rate() / 10 / float64(runtime.NumCPU()) diff --git a/lib/api/mocked_connections_test.go b/lib/api/mocked_connections_test.go index 54aee73f..608a1264 100644 --- a/lib/api/mocked_connections_test.go +++ b/lib/api/mocked_connections_test.go @@ -6,9 +6,17 @@ package api +import ( + "github.com/syncthing/syncthing/lib/connections" +) + type mockedConnections struct{} -func (m *mockedConnections) Status() map[string]interface{} { +func (m *mockedConnections) ListenerStatus() map[string]connections.ListenerStatusEntry { + return nil +} + +func (m *mockedConnections) ConnectionStatus() map[string]connections.ConnectionStatusEntry { return nil } diff --git a/lib/connections/service.go b/lib/connections/service.go index f0a49c22..95547f2e 100644 --- a/lib/connections/service.go +++ b/lib/connections/service.go @@ -90,10 +90,22 @@ var tlsVersionNames = map[uint16]string{ // dialers. Successful connections are handed to the model. type Service interface { suture.Service - Status() map[string]interface{} + ListenerStatus() map[string]ListenerStatusEntry + ConnectionStatus() map[string]ConnectionStatusEntry NATType() string } +type ListenerStatusEntry struct { + Error *string `json:"error"` + LANAddresses []string `json:"lanAddresses"` + WANAddresses []string `json:"wanAddresses"` +} + +type ConnectionStatusEntry struct { + When time.Time `json:"when"` + Error *string `json:"error"` +} + type service struct { *suture.Supervisor cfg config.Wrapper @@ -112,6 +124,9 @@ type service struct { listeners map[string]genericListener listenerTokens map[string]suture.ServiceToken listenerSupervisor *suture.Supervisor + + connectionStatusMut sync.RWMutex + connectionStatus map[string]ConnectionStatusEntry // address -> latest error/status } func NewService(cfg config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder, @@ -151,6 +166,9 @@ func NewService(cfg config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *t FailureBackoff: 600 * time.Second, PassThroughPanics: true, }), + + connectionStatusMut: sync.NewRWMutex(), + connectionStatus: make(map[string]ConnectionStatusEntry), } cfg.Subscribe(service) @@ -378,18 +396,23 @@ func (s *service) connect() { uri, err := url.Parse(addr) if err != nil { + s.setConnectionStatus(addr, err) l.Infof("Parsing dialer address %s: %v", addr, err) continue } if len(deviceCfg.AllowedNetworks) > 0 { if !IsAllowedNetwork(uri.Host, deviceCfg.AllowedNetworks) { + s.setConnectionStatus(addr, errors.New("network disallowed")) l.Debugln("Network for", uri, "is disallowed") continue } } dialerFactory, err := getDialerFactory(cfg, uri) + if err != nil { + s.setConnectionStatus(addr, err) + } switch err { case nil: // all good @@ -424,6 +447,7 @@ func (s *service) connect() { } dialTargets = append(dialTargets, dialTarget{ + addr: addr, dialer: dialer, priority: priority, deviceID: deviceID, @@ -431,7 +455,7 @@ func (s *service) connect() { }) } - conn, ok := dialParallel(deviceCfg.DeviceID, dialTargets) + conn, ok := s.dialParallel(deviceCfg.DeviceID, dialTargets) if ok { s.conns <- conn } @@ -633,19 +657,19 @@ func (s *service) ExternalAddresses() []string { return util.UniqueStrings(addrs) } -func (s *service) Status() map[string]interface{} { +func (s *service) ListenerStatus() map[string]ListenerStatusEntry { + result := make(map[string]ListenerStatusEntry) s.listenersMut.RLock() - result := make(map[string]interface{}) for addr, listener := range s.listeners { - status := make(map[string]interface{}) + var status ListenerStatusEntry - err := listener.Error() - if err != nil { - status["error"] = err.Error() + if err := listener.Error(); err != nil { + errStr := err.Error() + status.Error = &errStr } - status["lanAddresses"] = urlsToStrings(listener.LANAddresses()) - status["wanAddresses"] = urlsToStrings(listener.WANAddresses()) + status.LANAddresses = urlsToStrings(listener.LANAddresses()) + status.WANAddresses = urlsToStrings(listener.WANAddresses()) result[addr] = status } @@ -653,6 +677,28 @@ func (s *service) Status() map[string]interface{} { return result } +func (s *service) ConnectionStatus() map[string]ConnectionStatusEntry { + result := make(map[string]ConnectionStatusEntry) + s.connectionStatusMut.RLock() + for k, v := range s.connectionStatus { + result[k] = v + } + s.connectionStatusMut.RUnlock() + return result +} + +func (s *service) setConnectionStatus(address string, err error) { + status := ConnectionStatusEntry{When: time.Now().UTC().Truncate(time.Second)} + if err != nil { + errStr := err.Error() + status.Error = &errStr + } + + s.connectionStatusMut.Lock() + s.connectionStatus[address] = status + s.connectionStatusMut.Unlock() +} + func (s *service) NATType() string { s.listenersMut.RLock() defer s.listenersMut.RUnlock() @@ -769,7 +815,7 @@ func IsAllowedNetwork(host string, allowed []string) bool { return false } -func dialParallel(deviceID protocol.DeviceID, dialTargets []dialTarget) (internalConn, bool) { +func (s *service) dialParallel(deviceID protocol.DeviceID, dialTargets []dialTarget) (internalConn, bool) { // Group targets into buckets by priority dialTargetBuckets := make(map[int][]dialTarget, len(dialTargets)) for _, tgt := range dialTargets { @@ -793,6 +839,7 @@ func dialParallel(deviceID protocol.DeviceID, dialTargets []dialTarget) (interna wg.Add(1) go func(tgt dialTarget) { conn, err := tgt.Dial() + s.setConnectionStatus(tgt.addr, err) if err == nil { res <- conn } diff --git a/lib/connections/structs.go b/lib/connections/structs.go index dcd3002e..cdb14c08 100644 --- a/lib/connections/structs.go +++ b/lib/connections/structs.go @@ -195,6 +195,7 @@ func (o *onAddressesChangedNotifier) notifyAddressesChanged(l genericListener) { } type dialTarget struct { + addr string dialer genericDialer priority int uri *url.URL