diff --git a/Duplicati/Library/Main/Backend/BackendManager.Handler.cs b/Duplicati/Library/Main/Backend/BackendManager.Handler.cs index 5b806218f..eba0bafa3 100644 --- a/Duplicati/Library/Main/Backend/BackendManager.Handler.cs +++ b/Duplicati/Library/Main/Backend/BackendManager.Handler.cs @@ -424,11 +424,11 @@ partial class BackendManager // Refresh DNS name if we fail to connect in order to prevent issues with incorrect DNS entries var dnsFailure = Library.Utility.ExceptionExtensions.FlattenException(ex) - .Any(x => - (x is System.Net.WebException wex && wex.Status == System.Net.WebExceptionStatus.NameResolutionFailure) - || - (x is System.Net.Sockets.SocketException sockEx && sockEx.SocketErrorCode == System.Net.Sockets.SocketError.HostNotFound) - ); + .Any(x => + (x is System.Net.WebException wex && wex.Status == System.Net.WebExceptionStatus.NameResolutionFailure) + || + (x is System.Net.Sockets.SocketException sockEx && sockEx.SocketErrorCode == System.Net.Sockets.SocketError.HostNotFound) + ); if (dnsFailure) { try @@ -459,10 +459,17 @@ partial class BackendManager if (!recovered && retries <= maxRetries && retryDelay.Ticks != 0) { var delay = Library.Utility.Utility.GetRetryDelay(retryDelay, retries, retryWithExponentialBackoff); - await Task.Delay(delay, context.TaskReader.ProgressToken).ConfigureAwait(false); + using var ct = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, context.TaskReader.ProgressToken, context.TaskReader.TransferToken, context.TaskReader.StopToken); + await Task.Delay(delay, ct.Token).ConfigureAwait(false); } } + if (!await context.TaskReader.ProgressRendevouz()) + { + op.SetCancelled(); + return; + } + } while (retries <= maxRetries); // If we have a last exception, we failed diff --git a/Duplicati/Library/Main/Operation/Common/TaskControl.cs b/Duplicati/Library/Main/Operation/Common/TaskControl.cs index 1c02eb455..cd35da405 100644 --- a/Duplicati/Library/Main/Operation/Common/TaskControl.cs +++ b/Duplicati/Library/Main/Operation/Common/TaskControl.cs @@ -1,22 +1,22 @@ -// Copyright (C) 2025, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// Copyright (C) 2025, The Duplicati Team +// https://duplicati.com, hello@duplicati.com +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; @@ -52,6 +52,11 @@ namespace Duplicati.Library.Main.Operation.Common /// true if the transfer should continue, false if it should stop Task TransferRendevouz(); + /// + /// A cancellation token that can be used to monitor stop requests + /// + CancellationToken StopToken { get; } + #if DEBUG /// /// Callback for testing the task control @@ -142,6 +147,11 @@ namespace Duplicati.Library.Main.Operation.Common /// private readonly CancellationTokenSource m_transferTcs = new(); + /// + /// Cancellation token source that is cancelled if the operation is stopped + /// + private readonly CancellationTokenSource m_stopTcs = new CancellationTokenSource(); + /// /// The control lock instance /// @@ -167,6 +177,11 @@ namespace Duplicati.Library.Main.Operation.Common /// public CancellationToken TransferToken => m_transferTcs.Token; + /// + /// A cancellation token that is cancelled if the operation is stopped + /// + public CancellationToken StopToken => m_stopTcs.Token; + /// /// Initializes a new instance of the class. /// @@ -269,6 +284,8 @@ namespace Duplicati.Library.Main.Operation.Common m_transfer.SetResult(false); m_transferstate = State.Stopped; } + + m_stopTcs.Cancel(); } } @@ -298,6 +315,8 @@ namespace Duplicati.Library.Main.Operation.Common m_transferstate = State.Terminated; m_transferTcs.Cancel(); } + + m_stopTcs.Cancel(); } }