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
This commit is contained in:
Jakob Borg
2019-05-16 23:11:46 +02:00
committed by Audrius Butkevicius
parent 1da9317a09
commit 2c866277a2
6 changed files with 88 additions and 15 deletions

View File

@@ -715,8 +715,14 @@
</span> </span>
</td> </td>
<td ng-if="!connections[deviceCfg.deviceID].connected" class="text-right"> <td ng-if="!connections[deviceCfg.deviceID].connected" class="text-right">
<span ng-repeat="addr in deviceCfg.addresses"><span tooltip data-original-title="{{'Configured' | translate}}">{{addr}}</span><br></span> <span ng-repeat="addr in deviceCfg.addresses">
<span ng-repeat="addr in discoveryCache[deviceCfg.deviceID].addresses"><span tooltip data-original-title="{{'Discovered' | translate}}">{{addr}}</span><br></span> <span tooltip data-original-title="{{'Configured' | translate}}">{{addr}}</span><br>
<small ng-if="system.lastDialStatus[addr].error" tooltip data-original-title="{{system.lastDialStatus[addr].error}}" class="text-danger">{{abbreviatedError(addr)}}<br></small>
</span>
<span ng-repeat="addr in discoveryCache[deviceCfg.deviceID].addresses">
<span tooltip data-original-title="{{'Discovered' | translate}}">{{addr}}</span><br>
<small ng-if="system.lastDialStatus[addr].error" tooltip data-original-title="{{system.lastDialStatus[addr].error}}" class="text-danger">{{abbreviatedError(addr)}}<br></small>
</span>
</td> </td>
</tr> </tr>
<tr ng-if="connections[deviceCfg.deviceID].connected && connections[deviceCfg.deviceID].type.indexOf('Relay') > -1" tooltip data-original-title="Connections via relays might be rate limited by the relay"> <tr ng-if="connections[deviceCfg.deviceID].connected && connections[deviceCfg.deviceID].type.indexOf('Relay') > -1" tooltip data-original-title="Connections via relays might be rate limited by the relay">

View File

@@ -2406,4 +2406,14 @@ angular.module('syncthing.core')
$scope.saveConfig(); $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 + ")";
}
}); });

View File

@@ -898,7 +898,8 @@ func (s *service) getSystemStatus(w http.ResponseWriter, r *http.Request) {
res["discoveryErrors"] = discoErrors 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 // cpuUsage.Rate() is in milliseconds per second, so dividing by ten
// gives us percent // gives us percent
res["cpuPercent"] = s.cpu.Rate() / 10 / float64(runtime.NumCPU()) res["cpuPercent"] = s.cpu.Rate() / 10 / float64(runtime.NumCPU())

View File

@@ -6,9 +6,17 @@
package api package api
import (
"github.com/syncthing/syncthing/lib/connections"
)
type mockedConnections struct{} 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 return nil
} }

View File

@@ -90,10 +90,22 @@ var tlsVersionNames = map[uint16]string{
// dialers. Successful connections are handed to the model. // dialers. Successful connections are handed to the model.
type Service interface { type Service interface {
suture.Service suture.Service
Status() map[string]interface{} ListenerStatus() map[string]ListenerStatusEntry
ConnectionStatus() map[string]ConnectionStatusEntry
NATType() string 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 { type service struct {
*suture.Supervisor *suture.Supervisor
cfg config.Wrapper cfg config.Wrapper
@@ -112,6 +124,9 @@ type service struct {
listeners map[string]genericListener listeners map[string]genericListener
listenerTokens map[string]suture.ServiceToken listenerTokens map[string]suture.ServiceToken
listenerSupervisor *suture.Supervisor 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, 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, FailureBackoff: 600 * time.Second,
PassThroughPanics: true, PassThroughPanics: true,
}), }),
connectionStatusMut: sync.NewRWMutex(),
connectionStatus: make(map[string]ConnectionStatusEntry),
} }
cfg.Subscribe(service) cfg.Subscribe(service)
@@ -378,18 +396,23 @@ func (s *service) connect() {
uri, err := url.Parse(addr) uri, err := url.Parse(addr)
if err != nil { if err != nil {
s.setConnectionStatus(addr, err)
l.Infof("Parsing dialer address %s: %v", addr, err) l.Infof("Parsing dialer address %s: %v", addr, err)
continue continue
} }
if len(deviceCfg.AllowedNetworks) > 0 { if len(deviceCfg.AllowedNetworks) > 0 {
if !IsAllowedNetwork(uri.Host, deviceCfg.AllowedNetworks) { if !IsAllowedNetwork(uri.Host, deviceCfg.AllowedNetworks) {
s.setConnectionStatus(addr, errors.New("network disallowed"))
l.Debugln("Network for", uri, "is disallowed") l.Debugln("Network for", uri, "is disallowed")
continue continue
} }
} }
dialerFactory, err := getDialerFactory(cfg, uri) dialerFactory, err := getDialerFactory(cfg, uri)
if err != nil {
s.setConnectionStatus(addr, err)
}
switch err { switch err {
case nil: case nil:
// all good // all good
@@ -424,6 +447,7 @@ func (s *service) connect() {
} }
dialTargets = append(dialTargets, dialTarget{ dialTargets = append(dialTargets, dialTarget{
addr: addr,
dialer: dialer, dialer: dialer,
priority: priority, priority: priority,
deviceID: deviceID, 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 { if ok {
s.conns <- conn s.conns <- conn
} }
@@ -633,19 +657,19 @@ func (s *service) ExternalAddresses() []string {
return util.UniqueStrings(addrs) 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() s.listenersMut.RLock()
result := make(map[string]interface{})
for addr, listener := range s.listeners { for addr, listener := range s.listeners {
status := make(map[string]interface{}) var status ListenerStatusEntry
err := listener.Error() if err := listener.Error(); err != nil {
if err != nil { errStr := err.Error()
status["error"] = err.Error() status.Error = &errStr
} }
status["lanAddresses"] = urlsToStrings(listener.LANAddresses()) status.LANAddresses = urlsToStrings(listener.LANAddresses())
status["wanAddresses"] = urlsToStrings(listener.WANAddresses()) status.WANAddresses = urlsToStrings(listener.WANAddresses())
result[addr] = status result[addr] = status
} }
@@ -653,6 +677,28 @@ func (s *service) Status() map[string]interface{} {
return result 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 { func (s *service) NATType() string {
s.listenersMut.RLock() s.listenersMut.RLock()
defer s.listenersMut.RUnlock() defer s.listenersMut.RUnlock()
@@ -769,7 +815,7 @@ func IsAllowedNetwork(host string, allowed []string) bool {
return false 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 // Group targets into buckets by priority
dialTargetBuckets := make(map[int][]dialTarget, len(dialTargets)) dialTargetBuckets := make(map[int][]dialTarget, len(dialTargets))
for _, tgt := range dialTargets { for _, tgt := range dialTargets {
@@ -793,6 +839,7 @@ func dialParallel(deviceID protocol.DeviceID, dialTargets []dialTarget) (interna
wg.Add(1) wg.Add(1)
go func(tgt dialTarget) { go func(tgt dialTarget) {
conn, err := tgt.Dial() conn, err := tgt.Dial()
s.setConnectionStatus(tgt.addr, err)
if err == nil { if err == nil {
res <- conn res <- conn
} }

View File

@@ -195,6 +195,7 @@ func (o *onAddressesChangedNotifier) notifyAddressesChanged(l genericListener) {
} }
type dialTarget struct { type dialTarget struct {
addr string
dialer genericDialer dialer genericDialer
priority int priority int
uri *url.URL uri *url.URL