Files
duplicati/Duplicati/Library/RestAPI/LiveControls.cs
T

434 lines
16 KiB
C#
Raw Normal View History

2025-01-07 09:40:39 +01:00
// Copyright (C) 2025, The Duplicati Team
2024-06-05 11:02:56 +02:00
// https://duplicati.com, hello@duplicati.com
//
2024-06-05 11:02:56 +02:00
// 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:
//
2024-06-05 11:02:56 +02:00
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
2024-06-05 11:02:56 +02:00
// 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;
2024-06-09 21:33:48 +02:00
using System.Runtime.Versioning;
2024-03-15 16:51:01 +01:00
using Duplicati.Library.IO;
2024-06-09 22:12:03 +02:00
using Duplicati.Server.Database;
2018-11-02 22:13:25 +01:00
namespace Duplicati.Server
{
/// <summary>
/// This class keeps track of the users modifications regarding
/// throttling and pause/resume
/// </summary>
2024-03-15 16:51:01 +01:00
public class LiveControls : ILiveControls
{
/// <summary>
2024-12-18 08:27:59 +01:00
/// Event that is activated the the live control state changes
/// </summary>
2024-12-18 08:27:59 +01:00
public sealed record LiveControlEvent
{
/// <summary>
/// The new state of the live control
/// </summary>
public required LiveControlState State { get; init; }
/// <summary>
/// A value that indicates if the transfers are paused
/// </summary>
public required bool TransfersPaused { get; init; }
/// <summary>
/// The time when processing will resume, or zero if paused indefinitely
/// </summary>
public required DateTime WaitTimeExpiration { get; init; }
}
/// <summary>
2024-12-18 08:27:59 +01:00
/// The tag used for logging
/// </summary>
2024-12-18 08:27:59 +01:00
private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType<LiveControls>();
/// <summary>
2024-12-18 08:27:59 +01:00
/// An callback that is activated when the pause state changes
/// </summary>
2024-12-18 08:27:59 +01:00
public Action<LiveControlEvent> StateChanged;
/// <summary>
/// The possible states for the live control
/// </summary>
public enum LiveControlState
{
/// <summary>
/// Indicates that the backups are running
/// </summary>
Running,
/// <summary>
/// Indicates that the backups are currently suspended
/// </summary>
Paused
}
/// <summary>
/// The current control state
/// </summary>
private LiveControlState m_state;
2024-12-18 08:27:59 +01:00
/// <summary>
/// A value that indicates if the transfers are paused
/// </summary>
private bool m_transfersPaused = false;
/// <summary>
/// A value that indicates if the current pause state is caused by being suspended
/// </summary>
private bool m_pausedForSuspend = false;
/// <summary>
/// The time to pause for, used to ensure that a user set pause can override the suspend pause
/// </summary>
2025-03-19 09:14:34 +01:00
private DateTime m_suspendMinimumPause = new DateTime(0, DateTimeKind.Utc);
/// <summary>
/// Gets the current state for the control
/// </summary>
public LiveControlState State { get { return m_state; } }
/// <summary>
2024-12-18 08:27:59 +01:00
/// Gets a value that indicates if the backups are running
/// </summary>
2024-12-18 08:27:59 +01:00
public bool IsPaused => State == LiveControlState.Paused;
/// <summary>
2024-12-18 08:27:59 +01:00
/// Gets a value that indicates if the transfers are paused
/// </summary>
2024-12-18 08:27:59 +01:00
public bool TransfersPaused => m_transfersPaused;
/// <summary>
/// The object that ensures concurrent operations
/// </summary>
private readonly object m_lock = new object();
/// <summary>
/// The timer that is activated after a pause period.
/// </summary>
2024-03-15 16:51:01 +01:00
private System.Threading.Timer m_waitTimer;
/// <summary>
/// The time that the current pause is expected to expire
/// </summary>
2025-03-19 09:14:34 +01:00
private DateTime m_waitTimeExpiration = new DateTime(0, DateTimeKind.Utc);
2024-06-09 22:12:03 +02:00
/// <summary>
/// The connection to use
/// </summary>
private readonly Connection m_connection;
/// <summary>
/// Constructs a new instance of the LiveControl
/// </summary>
2024-06-09 22:12:03 +02:00
/// <param name="connection">The connection to use</param>
public LiveControls(Connection connection)
2024-03-15 16:51:01 +01:00
{
2024-06-09 22:12:03 +02:00
m_connection = connection;
Init();
2024-03-15 16:51:01 +01:00
}
2025-03-19 09:14:34 +01:00
/// <summary>
/// Clamps the milliseconds to a valid range for the timer
/// </summary>
/// <param name="duration">The duration to clamp</param>
/// <returns>The clamped milliseconds</returns>
private static long ClampMilliseconds(TimeSpan duration)
{
if (duration.TotalMilliseconds < 100)
return 100;
if (duration > TimeSpan.FromHours(24))
return (long)TimeSpan.FromHours(24).TotalMilliseconds;
return (long)duration.TotalMilliseconds;
}
/// <summary>
2024-03-15 16:51:01 +01:00
/// Constructs a new instance of the LiveControl
/// </summary>
2024-06-09 22:12:03 +02:00
private void Init()
{
2024-06-09 22:12:03 +02:00
var settings = m_connection.ApplicationSettings;
m_state = LiveControlState.Running;
m_waitTimer = new System.Threading.Timer(m_waitTimer_Tick, this, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
if (!string.IsNullOrEmpty(settings.StartupDelayDuration) && settings.StartupDelayDuration != "0")
{
2025-03-19 09:14:34 +01:00
var startupDelay = new TimeSpan(0);
try { startupDelay = Library.Utility.Timeparser.ParseTimeSpan(settings.StartupDelayDuration); }
catch { }
2025-03-19 09:14:34 +01:00
if (startupDelay.Ticks > 0)
{
2025-03-19 09:14:34 +01:00
m_waitTimeExpiration = DateTime.UtcNow.Add(startupDelay);
m_waitTimer.Change(ClampMilliseconds(startupDelay), System.Threading.Timeout.Infinite);
m_state = LiveControlState.Paused;
}
}
2024-12-18 08:27:59 +01:00
var pausedUntil = settings.PausedUntil;
if (pausedUntil != null)
{
if (pausedUntil.Value.Ticks == 0)
{
2025-03-19 09:14:34 +01:00
m_waitTimeExpiration = new DateTime(0, DateTimeKind.Utc);
2024-12-18 08:27:59 +01:00
m_waitTimer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
m_state = LiveControlState.Paused;
}
2024-12-18 08:27:59 +01:00
else if (pausedUntil.Value > DateTime.UtcNow && pausedUntil.Value > m_waitTimeExpiration)
{
2025-03-19 09:14:34 +01:00
var period = pausedUntil.Value - DateTime.UtcNow;
if (period.TotalMilliseconds > 100)
2024-12-18 08:27:59 +01:00
{
m_waitTimeExpiration = pausedUntil.Value;
2025-03-19 09:14:34 +01:00
m_waitTimer.Change(ClampMilliseconds(period), System.Threading.Timeout.Infinite);
2024-12-18 08:27:59 +01:00
m_state = LiveControlState.Paused;
}
}
2024-12-18 08:27:59 +01:00
}
try
{
2024-05-30 01:50:02 +02:00
if (OperatingSystem.IsWindows())
RegisterHibernateMonitor();
}
catch { }
}
/// <summary>
/// Event that occurs when the timeout duration is exceeded
/// </summary>
/// <param name="sender">The sender of the event</param>
private void m_waitTimer_Tick(object sender)
{
lock (m_lock)
Resume();
}
2024-12-18 08:27:59 +01:00
/// <summary>
/// Creates a new event object
/// </summary>
/// <returns>A new event object</returns>
private LiveControlEvent CreateEvent()
{
lock (m_lock)
return new LiveControlEvent()
{
State = m_state,
TransfersPaused = m_transfersPaused,
WaitTimeExpiration = m_waitTimeExpiration
};
}
/// <summary>
/// Internal helper to reset the timeout timer
/// </summary>
/// <param name="timeout">The time to wait</param>
private void ResetTimer(string timeout)
{
lock (m_lock)
if (!string.IsNullOrEmpty(timeout))
{
2025-03-19 09:14:34 +01:00
var delay = Library.Utility.Timeparser.ParseTimeSpan(timeout);
m_waitTimeExpiration = DateTime.UtcNow.Add(delay);
m_waitTimer.Change(ClampMilliseconds(delay), System.Threading.Timeout.Infinite);
}
else
{
2025-03-19 09:14:34 +01:00
m_waitTimeExpiration = new DateTime(0, DateTimeKind.Utc);
m_waitTimer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
}
}
/// <summary>
/// Internal helper to set the pause mode
/// </summary>
private void SetPauseMode()
{
2024-12-18 08:27:59 +01:00
LiveControlEvent ev = null;
lock (m_lock)
{
if (m_state == LiveControlState.Running)
{
m_state = LiveControlState.Paused;
if (StateChanged != null)
2024-12-18 08:27:59 +01:00
ev = CreateEvent();
}
}
2024-12-18 08:27:59 +01:00
if (ev != null)
StateChanged(ev);
}
/// <summary>
/// Pauses the backups until resumed
/// </summary>
2024-12-18 08:27:59 +01:00
public void Pause(bool alsoTransfers)
{
2024-12-18 08:27:59 +01:00
LiveControlEvent ev = null;
lock (m_lock)
{
2024-12-18 08:27:59 +01:00
m_transfersPaused = alsoTransfers;
2025-03-19 09:14:34 +01:00
m_waitTimeExpiration = new DateTime(0, DateTimeKind.Utc);
ResetTimer(null);
2024-12-18 08:27:59 +01:00
if (m_state == LiveControlState.Paused)
ev = StateChanged == null ? null : CreateEvent();
else
SetPauseMode();
}
2024-12-18 08:27:59 +01:00
if (ev != null)
StateChanged(ev);
}
/// <summary>
/// Resumes a backups to the running state
/// </summary>
public void Resume()
{
2024-12-18 08:27:59 +01:00
LiveControlEvent ev = null;
lock (m_lock)
{
if (m_state == LiveControlState.Paused)
{
//Make sure that the timer is cleared
ResetTimer(null);
2024-12-18 08:27:59 +01:00
m_transfersPaused = false;
2025-03-19 09:14:34 +01:00
m_waitTimeExpiration = new DateTime(0, DateTimeKind.Utc);
m_state = LiveControlState.Running;
if (StateChanged != null)
2024-12-18 08:27:59 +01:00
ev = CreateEvent();
}
}
2024-12-18 08:27:59 +01:00
if (ev != null)
StateChanged(ev);
}
/// <summary>
/// Suspends the backups for a given period
/// </summary>
/// <param name="timeout">The duration to wait</param>
2024-12-18 08:27:59 +01:00
/// <param name="alsoTransfers">If true, also pause the transfers</param>
public void Pause(string timeout, bool alsoTransfers)
{
2024-12-18 08:27:59 +01:00
Pause(Duplicati.Library.Utility.Timeparser.ParseTimeSpan(timeout), alsoTransfers);
}
/// <summary>
/// Suspends the backups for a given period
/// </summary>
/// <param name="timeout">The duration to wait</param>
2024-12-18 08:27:59 +01:00
/// <param name="alsoTransfers">If true, also pause the transfers</param>
public void Pause(TimeSpan timeout, bool alsoTransfers)
{
2024-12-18 08:27:59 +01:00
LiveControlEvent ev = null;
lock (m_lock)
{
2025-03-19 09:14:34 +01:00
m_waitTimeExpiration = DateTime.UtcNow.Add(timeout);
m_waitTimer.Change(ClampMilliseconds(timeout), System.Threading.Timeout.Infinite);
2024-12-18 08:27:59 +01:00
m_transfersPaused = alsoTransfers;
//We change the time, so we issue a new event
2024-12-18 08:27:59 +01:00
if (m_state == LiveControlState.Paused)
ev = StateChanged == null ? null : CreateEvent();
else
SetPauseMode();
}
2024-12-18 08:27:59 +01:00
if (ev != null)
StateChanged(ev);
}
/// <summary>
/// Gets the time the current pause is expected to end
/// </summary>
public DateTime EstimatedPauseEnd { get { return m_waitTimeExpiration; } }
/// <summary>
/// Method for calling a Win32 API
/// </summary>
[SupportedOSPlatform("windows")]
private void RegisterHibernateMonitor()
{
Microsoft.Win32.SystemEvents.PowerModeChanged += new Microsoft.Win32.PowerModeChangedEventHandler(SystemEvents_PowerModeChanged);
}
/// <summary>
/// A monitor for detecting when the system hibernates or resumes
/// </summary>
/// <param name="sender">Unused sender parameter</param>
/// <param name="_e">The event information</param>
[SupportedOSPlatform("windows")]
private void SystemEvents_PowerModeChanged(object sender, object _e)
{
Microsoft.Win32.PowerModeChangedEventArgs e = _e as Microsoft.Win32.PowerModeChangedEventArgs;
if (e == null)
return;
if (e.Mode == Microsoft.Win32.PowerModes.Suspend)
{
//If we are running, register as being paused due to suspending
if (this.m_state == LiveControlState.Running)
{
this.SetPauseMode();
m_pausedForSuspend = true;
2025-03-19 09:14:34 +01:00
m_suspendMinimumPause = new DateTime(0, DateTimeKind.Utc);
}
else
{
if (m_waitTimeExpiration.Ticks != 0)
{
m_pausedForSuspend = true;
m_suspendMinimumPause = this.EstimatedPauseEnd;
ResetTimer(null);
}
}
}
else if (e.Mode == Microsoft.Win32.PowerModes.Resume)
{
//If we have been been paused due to suspending, we un-pause now
if (m_pausedForSuspend)
{
2025-03-19 09:14:34 +01:00
long delayTicks = (m_suspendMinimumPause - DateTime.UtcNow).Ticks;
2024-06-09 22:12:03 +02:00
var appset = m_connection.ApplicationSettings;
if (!string.IsNullOrEmpty(appset.StartupDelayDuration) && appset.StartupDelayDuration != "0")
try { delayTicks = Math.Max(delayTicks, Library.Utility.Timeparser.ParseTimeSpan(appset.StartupDelayDuration).Ticks); }
catch { }
if (delayTicks > 0)
{
2024-12-18 08:27:59 +01:00
this.Pause(TimeSpan.FromTicks(delayTicks), true);
}
else
{
this.Resume();
}
}
m_pausedForSuspend = false;
2025-03-19 09:14:34 +01:00
m_suspendMinimumPause = new DateTime(0, DateTimeKind.Utc);
}
}
}
}