diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index b4e048b04..7d177e857 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -748,8 +748,13 @@ func autoUpgrade(cfg config.Wrapper, app *syncthing.App, evLogger events.Logger) continue } sub.Unsubscribe() + restartDelay := time.Minute + evLogger.Log(events.UpgradeRestartScheduled, map[string]any{ + "delayS": int(restartDelay / time.Second), + "newVersion": rel.Tag, + }) slog.Error("Automatically upgraded, restarting in 1 minute", slog.String("newVersion", rel.Tag)) - time.Sleep(time.Minute) + time.Sleep(restartDelay) app.Stop(svcutil.ExitUpgrade) return } diff --git a/gui/default/syncthing/core/eventService.js b/gui/default/syncthing/core/eventService.js index 747fa7b52..f8d53e94b 100644 --- a/gui/default/syncthing/core/eventService.js +++ b/gui/default/syncthing/core/eventService.js @@ -73,6 +73,7 @@ angular.module('syncthing.core') CLUSTER_CONFIG_RECEIVED: 'ClusterConfigReceived', // Emitted when receiving a remote device's cluster config DOWNLOAD_PROGRESS: 'DownloadProgress', // Emitted during file downloads for each folder for each file FAILURE: 'Failure', // Specific errors sent to the usage reporting server for diagnosis + UPGRADE_RESTART_SCHEDULED: 'UpgradeRestartScheduled', // Auto-upgrade completed, restart scheduled FOLDER_COMPLETION: 'FolderCompletion', //Emitted when the local or remote contents for a folder changes FOLDER_REJECTED: 'FolderRejected', // DEPRECATED: 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 PENDING_FOLDERS_CHANGED: 'PendingFoldersChanged', // Emitted when pending folders were added / updated (offered by some device, but not shared to them) or removed (folder ignored or added or no longer offered from the remote device) diff --git a/gui/default/syncthing/core/syncthingController.js b/gui/default/syncthing/core/syncthingController.js index ec0ab482f..7bee95bf0 100644 --- a/gui/default/syncthing/core/syncthingController.js +++ b/gui/default/syncthing/core/syncthingController.js @@ -11,6 +11,8 @@ angular.module('syncthing.core') var navigatingAway = false; var online = false; var restarting = false; + var restartExpectedFrom = 0; + var restartExpectedUntil = 0; function initController() { LocaleService.autoConfigLocale(); @@ -33,6 +35,34 @@ angular.module('syncthing.core') Events.start(); } + function clearRestartExpectation() { + restartExpectedFrom = 0; + restartExpectedUntil = 0; + } + + function setRestartExpectation(delayS) { + var delay = delayS > 0 ? delayS : 60; + var delayMs = delay * 1000; + var earlyMs = 5 * 1000; + var graceMs = 60 * 1000; + var now = Date.now(); + + restartExpectedFrom = now + Math.max(0, delayMs - earlyMs); + restartExpectedUntil = now + delayMs + graceMs; + } + + function restartExpectedNow() { + if (!restartExpectedUntil) { + return false; + } + var now = Date.now(); + if (now > restartExpectedUntil) { + clearRestartExpectation(); + return false; + } + return now >= restartExpectedFrom; + } + // public/scope definitions // window.metadata is set in /meta.js which requires authentication @@ -204,6 +234,7 @@ angular.module('syncthing.core') online = true; restarting = false; + clearRestartExpectation(); hideModal('#networkError'); hideModal('#restarting'); hideModal('#shutdown'); @@ -218,10 +249,26 @@ angular.module('syncthing.core') console.log('UIOffline'); online = false; if (!restarting) { - showModal('#networkError'); + if (restartExpectedNow()) { + restarting = true; + showModal('#restarting'); + } else { + showModal('#networkError'); + } } }); + $scope.$on(Events.UPGRADE_RESTART_SCHEDULED, function (_event, arg) { + var delayS = 0; + if (arg && arg.data && arg.data.delayS !== undefined) { + delayS = parseInt(arg.data.delayS, 10); + if (isNaN(delayS) || delayS < 0) { + delayS = 0; + } + } + setRestartExpectation(delayS); + }); + $scope.$on('HTTPError', function (event, arg) { // Emitted when a HTTP call fails. We use the status code to try // to figure out what's wrong. diff --git a/lib/events/events.go b/lib/events/events.go index 5712045e3..1504db40e 100644 --- a/lib/events/events.go +++ b/lib/events/events.go @@ -57,6 +57,7 @@ const ( ListenAddressesChanged LoginAttempt Failure + UpgradeRestartScheduled AllEvents = (1 << iota) - 1 ) @@ -134,6 +135,8 @@ func (t EventType) String() string { return "FolderWatchStateChanged" case Failure: return "Failure" + case UpgradeRestartScheduled: + return "UpgradeRestartScheduled" default: return "Unknown" } @@ -221,6 +224,8 @@ func UnmarshalEventType(s string) EventType { return FolderWatchStateChanged case "Failure": return Failure + case "UpgradeRestartScheduled": + return UpgradeRestartScheduled default: return 0 }