Pause and resume devices (ref #215)

This commit is contained in:
Jakob Borg 2015-08-23 21:56:10 +02:00
parent 1e447741ee
commit 944d9c84a0
10 changed files with 154 additions and 23 deletions

View File

@ -158,6 +158,10 @@ next:
l.Infof("Connected to already connected device (%s)", remoteID) l.Infof("Connected to already connected device (%s)", remoteID)
c.Conn.Close() c.Conn.Close()
continue continue
} else if s.model.IsPaused(remoteID) {
l.Infof("Connection from paused device (%s)", remoteID)
c.Conn.Close()
continue
} }
for deviceID, deviceCfg := range s.cfg.Devices() { for deviceID, deviceCfg := range s.cfg.Devices() {
@ -235,6 +239,10 @@ func (s *connectionSvc) connect() {
continue continue
} }
if s.model.IsPaused(deviceID) {
continue
}
connected := s.model.ConnectedTo(deviceID) connected := s.model.ConnectedTo(deviceID)
s.mut.RLock() s.mut.RLock()

View File

@ -169,6 +169,8 @@ func (s *apiSvc) Serve() {
postRestMux.HandleFunc("/rest/system/restart", s.postSystemRestart) // - postRestMux.HandleFunc("/rest/system/restart", s.postSystemRestart) // -
postRestMux.HandleFunc("/rest/system/shutdown", s.postSystemShutdown) // - postRestMux.HandleFunc("/rest/system/shutdown", s.postSystemShutdown) // -
postRestMux.HandleFunc("/rest/system/upgrade", s.postSystemUpgrade) // - postRestMux.HandleFunc("/rest/system/upgrade", s.postSystemUpgrade) // -
postRestMux.HandleFunc("/rest/system/pause", s.postSystemPause) // device
postRestMux.HandleFunc("/rest/system/resume", s.postSystemResume) // device
// Debug endpoints, not for general use // Debug endpoints, not for general use
getRestMux.HandleFunc("/rest/debug/peerCompletion", s.getPeerCompletion) getRestMux.HandleFunc("/rest/debug/peerCompletion", s.getPeerCompletion)
@ -833,6 +835,32 @@ func (s *apiSvc) postSystemUpgrade(w http.ResponseWriter, r *http.Request) {
} }
} }
func (s *apiSvc) postSystemPause(w http.ResponseWriter, r *http.Request) {
var qs = r.URL.Query()
var deviceStr = qs.Get("device")
device, err := protocol.DeviceIDFromString(deviceStr)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
s.model.PauseDevice(device)
}
func (s *apiSvc) postSystemResume(w http.ResponseWriter, r *http.Request) {
var qs = r.URL.Query()
var deviceStr = qs.Get("device")
device, err := protocol.DeviceIDFromString(deviceStr)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
s.model.ResumeDevice(device)
}
func (s *apiSvc) postDBScan(w http.ResponseWriter, r *http.Request) { func (s *apiSvc) postDBScan(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query() qs := r.URL.Query()
folder := qs.Get("folder") folder := qs.Get("folder")

View File

@ -123,6 +123,15 @@ func (s *verboseSvc) formatEvent(ev events.Event) string {
delete(sum, "ignorePatterns") delete(sum, "ignorePatterns")
delete(sum, "stateChanged") delete(sum, "stateChanged")
return fmt.Sprintf("Summary for folder %q is %v", data["folder"], data["summary"]) return fmt.Sprintf("Summary for folder %q is %v", data["folder"], data["summary"])
case events.DevicePaused:
data := ev.Data.(map[string]string)
device := data["device"]
return fmt.Sprintf("Device %v was paused", device)
case events.DeviceResumed:
data := ev.Data.(map[string]string)
device := data["device"]
return fmt.Sprintf("Device %v was resumed", device)
} }
return fmt.Sprintf("%s %#v", ev.Type, ev) return fmt.Sprintf("%s %#v", ev.Type, ev)

View File

@ -113,6 +113,8 @@
"Override Changes": "Override Changes", "Override Changes": "Override Changes",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for", "Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Path where versions should be stored (leave empty for the default .stversions folder in the folder).", "Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Path where versions should be stored (leave empty for the default .stversions folder in the folder).",
"Pause": "Pause",
"Paused": "Paused",
"Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.", "Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.",
"Please wait": "Please wait", "Please wait": "Please wait",
"Preview": "Preview", "Preview": "Preview",
@ -130,6 +132,7 @@
"Restart": "Restart", "Restart": "Restart",
"Restart Needed": "Restart Needed", "Restart Needed": "Restart Needed",
"Restarting": "Restarting", "Restarting": "Restarting",
"Resume": "Resume",
"Reused": "Reused", "Reused": "Reused",
"Save": "Save", "Save": "Save",
"Scanning": "Scanning", "Scanning": "Scanning",

View File

@ -425,6 +425,7 @@
<span ng-switch-when="syncing"> <span ng-switch-when="syncing">
<span class="hidden-xs" translate>Syncing</span> ({{completion[deviceCfg.deviceID]._total | number:0}}%) <span class="hidden-xs" translate>Syncing</span> ({{completion[deviceCfg.deviceID]._total | number:0}}%)
</span> </span>
<span ng-switch-when="paused"><span class="hidden-xs" translate>Paused</span><span class="visible-xs">&#9724;</span></span>
<span ng-switch-when="disconnected"><span class="hidden-xs" translate>Disconnected</span><span class="visible-xs">&#9724;</span></span> <span ng-switch-when="disconnected"><span class="hidden-xs" translate>Disconnected</span><span class="visible-xs">&#9724;</span></span>
<span ng-switch-when="unused"><span class="hidden-xs" translate>Unused</span><span class="visible-xs">&#9724;</span></span> <span ng-switch-when="unused"><span class="hidden-xs" translate>Unused</span><span class="visible-xs">&#9724;</span></span>
</span> </span>
@ -434,18 +435,18 @@
<div class="panel-body"> <div class="panel-body">
<table class="table table-condensed table-striped"> <table class="table table-condensed table-striped">
<tbody> <tbody>
<tr ng-if="connections[deviceCfg.deviceID]"> <tr ng-if="connections[deviceCfg.deviceID].connected">
<th><span class="fa fa-fw fa-cloud-download"></span>&nbsp;<span translate>Download Rate</span></th> <th><span class="fa fa-fw fa-cloud-download"></span>&nbsp;<span translate>Download Rate</span></th>
<td class="text-right">{{connections[deviceCfg.deviceID].inbps | binary}}B/s ({{connections[deviceCfg.deviceID].inBytesTotal | binary}}B)</td> <td class="text-right">{{connections[deviceCfg.deviceID].inbps | binary}}B/s ({{connections[deviceCfg.deviceID].inBytesTotal | binary}}B)</td>
</tr> </tr>
<tr ng-if="connections[deviceCfg.deviceID]"> <tr ng-if="connections[deviceCfg.deviceID].connected">
<th><span class="fa fa-fw fa-cloud-upload"></span>&nbsp;<span translate>Upload Rate</span></th> <th><span class="fa fa-fw fa-cloud-upload"></span>&nbsp;<span translate>Upload Rate</span></th>
<td class="text-right">{{connections[deviceCfg.deviceID].outbps | binary}}B/s ({{connections[deviceCfg.deviceID].outBytesTotal | binary}}B)</td> <td class="text-right">{{connections[deviceCfg.deviceID].outbps | binary}}B/s ({{connections[deviceCfg.deviceID].outBytesTotal | binary}}B)</td>
</tr> </tr>
<tr> <tr>
<th> <th>
<span class="fa fa-fw fa-link"></span> <span class="fa fa-fw fa-link"></span>
<span translate ng-if="connections[deviceCfg.deviceID].type.indexOf('basic') == 0" >Address</span> <span translate ng-if="connections[deviceCfg.deviceID].type.indexOf('direct') == 0" >Address</span>
<span translate ng-if="connections[deviceCfg.deviceID].type.indexOf('relay') == 0" >Relayed via</span> <span translate ng-if="connections[deviceCfg.deviceID].type.indexOf('relay') == 0" >Relayed via</span>
</th> </th>
<td class="text-right">{{deviceAddr(deviceCfg)}}</td> <td class="text-right">{{deviceAddr(deviceCfg)}}</td>
@ -461,11 +462,11 @@
<th><span class="fa fa-fw fa-thumbs-o-up"></span>&nbsp;<span translate>Introducer</span></th> <th><span class="fa fa-fw fa-thumbs-o-up"></span>&nbsp;<span translate>Introducer</span></th>
<td translate class="text-right">Yes</td> <td translate class="text-right">Yes</td>
</tr> </tr>
<tr ng-if="connections[deviceCfg.deviceID]"> <tr ng-if="connections[deviceCfg.deviceID].version">
<th><span class="fa fa-fw fa-tag"></span>&nbsp;<span translate>Version</span></th> <th><span class="fa fa-fw fa-tag"></span>&nbsp;<span translate>Version</span></th>
<td class="text-right">{{connections[deviceCfg.deviceID].clientVersion}}</td> <td class="text-right">{{connections[deviceCfg.deviceID].clientVersion}}</td>
</tr> </tr>
<tr ng-if="!connections[deviceCfg.deviceID]"> <tr ng-if="!connections[deviceCfg.deviceID].connected">
<th><span class="fa fa-fw fa-eye"></span>&nbsp;<span translate>Last seen</span></th> <th><span class="fa fa-fw fa-eye"></span>&nbsp;<span translate>Last seen</span></th>
<td translate ng-if="!deviceStats[deviceCfg.deviceID].lastSeenDays || deviceStats[deviceCfg.deviceID].lastSeenDays >= 365" class="text-right">Never</td> <td translate ng-if="!deviceStats[deviceCfg.deviceID].lastSeenDays || deviceStats[deviceCfg.deviceID].lastSeenDays >= 365" class="text-right">Never</td>
<td ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays < 365" class="text-right">{{deviceStats[deviceCfg.deviceID].lastSeen | date:"yyyy-MM-dd HH:mm:ss"}}</td> <td ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays < 365" class="text-right">{{deviceStats[deviceCfg.deviceID].lastSeen | date:"yyyy-MM-dd HH:mm:ss"}}</td>
@ -482,6 +483,12 @@
<button type="button" class="btn btn-sm btn-default" ng-click="editDevice(deviceCfg)"> <button type="button" class="btn btn-sm btn-default" ng-click="editDevice(deviceCfg)">
<span class="fa fa-pencil"></span>&nbsp;<span translate>Edit</span> <span class="fa fa-pencil"></span>&nbsp;<span translate>Edit</span>
</button> </button>
<button ng-if="!connections[deviceCfg.deviceID].paused" type="button" class="btn btn-sm btn-default" ng-click="pauseDevice(deviceCfg.deviceID)">
<span class="fa fa-pause"></span>&nbsp;<span translate>Pause</span>
</button>
<button ng-if="connections[deviceCfg.deviceID].paused" type="button" class="btn btn-sm btn-default" ng-click="resumeDevice(deviceCfg.deviceID)">
<span class="fa fa-play"></span>&nbsp;<span translate>Resume</span>
</button>
</span> </span>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>

View File

@ -63,6 +63,8 @@ angular.module('syncthing.core')
DEVICE_DISCONNECTED: 'DeviceDisconnected', // Generated each time a connection to a device has been terminated DEVICE_DISCONNECTED: 'DeviceDisconnected', // Generated each time a connection to a device has been terminated
DEVICE_DISCOVERED: 'DeviceDiscovered', // Emitted when a new device is discovered using local discovery DEVICE_DISCOVERED: 'DeviceDiscovered', // Emitted when a new device is discovered using local discovery
DEVICE_REJECTED: 'DeviceRejected', // Emitted when there is a connection from a device we are not configured to talk to DEVICE_REJECTED: 'DeviceRejected', // Emitted when there is a connection from a device we are not configured to talk to
DEVICE_PAUSED: 'DevicePaused', // Emitted when a device has been paused
DEVICE_RESUMED: 'DeviceResumed', // Emitted when a device has been resumed
DOWNLOAD_PROGRESS: 'DownloadProgress', // Emitted during file downloads for each folder for each file DOWNLOAD_PROGRESS: 'DownloadProgress', // Emitted during file downloads for each folder for each file
FOLDER_COMPLETION: 'FolderCompletion', //Emitted when the local or remote contents for a folder changes FOLDER_COMPLETION: 'FolderCompletion', //Emitted when the local or remote contents for a folder changes
FOLDER_REJECTED: 'FolderRejected', // Emitted when a device sends index information for a folder we do not have, or have but do not share with the device in question FOLDER_REJECTED: 'FolderRejected', // Emitted when a device sends index information for a folder we do not have, or have but do not share with the device in question

View File

@ -165,7 +165,7 @@ angular.module('syncthing.core')
}); });
$scope.$on(Events.DEVICE_DISCONNECTED, function (event, arg) { $scope.$on(Events.DEVICE_DISCONNECTED, function (event, arg) {
delete $scope.connections[arg.data.id]; $scope.connections[arg.data.id].connected = false;
refreshDeviceStats(); refreshDeviceStats();
}); });
@ -209,6 +209,14 @@ angular.module('syncthing.core')
$scope.deviceRejections[arg.data.device] = arg; $scope.deviceRejections[arg.data.device] = arg;
}); });
$scope.$on(Events.DEVICE_PAUSED, function (event, arg) {
$scope.connections[arg.data.device].paused = true;
});
$scope.$on(Events.DEVICE_RESUMED, function (event, arg) {
$scope.connections[arg.data.device].paused = false;
});
$scope.$on(Events.FOLDER_REJECTED, function (event, arg) { $scope.$on(Events.FOLDER_REJECTED, function (event, arg) {
$scope.folderRejections[arg.data.folder + "-" + arg.data.device] = arg; $scope.folderRejections[arg.data.folder + "-" + arg.data.device] = arg;
}); });
@ -625,7 +633,15 @@ angular.module('syncthing.core')
return 'unused'; return 'unused';
} }
if ($scope.connections[deviceCfg.deviceID]) { if (typeof $scope.connections[deviceCfg.deviceID] === 'undefined') {
return 'unknown';
}
if ($scope.connections[deviceCfg.deviceID].paused) {
return 'paused';
}
if ($scope.connections[deviceCfg.deviceID].connected) {
if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) { if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
return 'insync'; return 'insync';
} else { } else {
@ -643,7 +659,15 @@ angular.module('syncthing.core')
return 'warning'; return 'warning';
} }
if ($scope.connections[deviceCfg.deviceID]) { if (typeof $scope.connections[deviceCfg.deviceID] === 'undefined') {
return 'info';
}
if ($scope.connections[deviceCfg.deviceID].paused) {
return 'default';
}
if ($scope.connections[deviceCfg.deviceID].connected) {
if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) { if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
return 'success'; return 'success';
} else { } else {
@ -657,7 +681,7 @@ angular.module('syncthing.core')
$scope.deviceAddr = function (deviceCfg) { $scope.deviceAddr = function (deviceCfg) {
var conn = $scope.connections[deviceCfg.deviceID]; var conn = $scope.connections[deviceCfg.deviceID];
if (conn) { if (conn && conn.connected) {
return conn.address; return conn.address;
} }
return '?'; return '?';
@ -702,6 +726,14 @@ angular.module('syncthing.core')
return device.deviceID.substr(0, 6); return device.deviceID.substr(0, 6);
}; };
$scope.pauseDevice = function (device) {
$http.post(urlbase + "/system/pause?device=" + device);
};
$scope.resumeDevice = function (device) {
$http.post(urlbase + "/system/resume?device=" + device);
};
$scope.editSettings = function () { $scope.editSettings = function () {
// Make a working copy // Make a working copy
$scope.tmpOptions = angular.copy($scope.config.options); $scope.tmpOptions = angular.copy($scope.config.options);

File diff suppressed because one or more lines are too long

View File

@ -25,6 +25,8 @@ const (
DeviceConnected DeviceConnected
DeviceDisconnected DeviceDisconnected
DeviceRejected DeviceRejected
DevicePaused
DeviceResumed
LocalIndexUpdated LocalIndexUpdated
RemoteIndexUpdated RemoteIndexUpdated
ItemStarted ItemStarted
@ -78,6 +80,10 @@ func (t EventType) String() string {
return "FolderCompletion" return "FolderCompletion"
case FolderErrors: case FolderErrors:
return "FolderErrors" return "FolderErrors"
case DevicePaused:
return "DevicePaused"
case DeviceResumed:
return "DeviceResumed"
default: default:
return "Unknown" return "Unknown"
} }

View File

@ -89,7 +89,8 @@ type Model struct {
conn map[protocol.DeviceID]Connection conn map[protocol.DeviceID]Connection
deviceVer map[protocol.DeviceID]string deviceVer map[protocol.DeviceID]string
pmut sync.RWMutex // protects conn and deviceVer devicePaused map[protocol.DeviceID]bool
pmut sync.RWMutex // protects the above
reqValidationCache map[string]time.Time // folder / file name => time when confirmed to exist reqValidationCache map[string]time.Time // folder / file name => time when confirmed to exist
rvmut sync.RWMutex // protects reqValidationCache rvmut sync.RWMutex // protects reqValidationCache
@ -131,6 +132,7 @@ func NewModel(cfg *config.Wrapper, id protocol.DeviceID, deviceName, clientName,
folderStatRefs: make(map[string]*stats.FolderStatisticsReference), folderStatRefs: make(map[string]*stats.FolderStatisticsReference),
conn: make(map[protocol.DeviceID]Connection), conn: make(map[protocol.DeviceID]Connection),
deviceVer: make(map[protocol.DeviceID]string), deviceVer: make(map[protocol.DeviceID]string),
devicePaused: make(map[protocol.DeviceID]bool),
reqValidationCache: make(map[string]time.Time), reqValidationCache: make(map[string]time.Time),
fmut: sync.NewRWMutex(), fmut: sync.NewRWMutex(),
@ -217,6 +219,8 @@ func (m *Model) StartFolderRO(folder string) {
type ConnectionInfo struct { type ConnectionInfo struct {
protocol.Statistics protocol.Statistics
Connected bool
Paused bool
Address string Address string
ClientVersion string ClientVersion string
Type ConnectionType Type ConnectionType
@ -227,9 +231,11 @@ func (info ConnectionInfo) MarshalJSON() ([]byte, error) {
"at": info.At, "at": info.At,
"inBytesTotal": info.InBytesTotal, "inBytesTotal": info.InBytesTotal,
"outBytesTotal": info.OutBytesTotal, "outBytesTotal": info.OutBytesTotal,
"connected": info.Connected,
"paused": info.Paused,
"address": info.Address, "address": info.Address,
"type": info.Type.String(),
"clientVersion": info.ClientVersion, "clientVersion": info.ClientVersion,
"type": info.Type.String(),
}) })
} }
@ -242,16 +248,21 @@ func (m *Model) ConnectionStats() map[string]interface{} {
m.pmut.RLock() m.pmut.RLock()
m.fmut.RLock() m.fmut.RLock()
var res = make(map[string]interface{}) res := make(map[string]interface{})
conns := make(map[string]ConnectionInfo, len(m.conn)) devs := m.cfg.Devices()
for device, conn := range m.conn { conns := make(map[string]ConnectionInfo, len(devs))
for device := range devs {
ci := ConnectionInfo{ ci := ConnectionInfo{
Statistics: conn.Statistics(),
ClientVersion: m.deviceVer[device], ClientVersion: m.deviceVer[device],
Paused: m.devicePaused[device],
} }
if addr := m.conn[device].RemoteAddr(); addr != nil { if conn, ok := m.conn[device]; ok {
ci.Address = addr.String()
ci.Type = conn.Type ci.Type = conn.Type
ci.Connected = ok
ci.Statistics = conn.Statistics()
if addr := conn.RemoteAddr(); addr != nil {
ci.Address = addr.String()
}
} }
conns[device.String()] = ci conns[device.String()] = ci
@ -956,6 +967,31 @@ func (m *Model) AddConnection(conn Connection) {
m.deviceWasSeen(deviceID) m.deviceWasSeen(deviceID)
} }
func (m *Model) PauseDevice(device protocol.DeviceID) {
m.pmut.Lock()
m.devicePaused[device] = true
_, ok := m.conn[device]
m.pmut.Unlock()
if ok {
m.Close(device, errors.New("device paused"))
}
events.Default.Log(events.DevicePaused, map[string]string{"device": device.String()})
}
func (m *Model) ResumeDevice(device protocol.DeviceID) {
m.pmut.Lock()
m.devicePaused[device] = false
m.pmut.Unlock()
events.Default.Log(events.DeviceResumed, map[string]string{"device": device.String()})
}
func (m *Model) IsPaused(device protocol.DeviceID) bool {
m.pmut.Lock()
paused := m.devicePaused[device]
m.pmut.Unlock()
return paused
}
func (m *Model) deviceStatRef(deviceID protocol.DeviceID) *stats.DeviceStatisticsReference { func (m *Model) deviceStatRef(deviceID protocol.DeviceID) *stats.DeviceStatisticsReference {
m.fmut.Lock() m.fmut.Lock()
defer m.fmut.Unlock() defer m.fmut.Unlock()