diff --git a/BuildTools/LicenseUpdater/LicenseUpdater.csproj b/BuildTools/LicenseUpdater/LicenseUpdater.csproj
index a44450dc4..551de1203 100755
--- a/BuildTools/LicenseUpdater/LicenseUpdater.csproj
+++ b/BuildTools/LicenseUpdater/LicenseUpdater.csproj
@@ -1,11 +1,11 @@
-
-
-
- Exe
- net8.0
- license_upgrader
- enable
- enable
-
-
-
+
+
+
+ Exe
+ net8.0
+ license_upgrader
+ enable
+ enable
+
+
+
diff --git a/Duplicati.Library.RestAPI/Abstractions/IScheduler.cs b/Duplicati.Library.RestAPI/Abstractions/IScheduler.cs
new file mode 100644
index 000000000..0d7016e78
--- /dev/null
+++ b/Duplicati.Library.RestAPI/Abstractions/IScheduler.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using Duplicati.Library.Utility;
+using Duplicati.Server;
+using Duplicati.Server.Serialization.Interface;
+
+namespace Duplicati.WebserverCore.Abstractions;
+
+public interface IScheduler
+{
+ ///
+ /// Initializes scheduler
+ ///
+ /// The worker thread
+ void Init(WorkerThread worker);
+
+ IList> GetSchedulerQueueIds();
+
+ ///
+ /// Terminates the thread. Any items still in queue will be removed
+ ///
+ /// True if the call should block until the thread has exited, false otherwise
+ void Terminate(bool wait);
+
+ ///
+ /// An event that is raised when the schedule changes
+ ///
+ event EventHandler NewSchedule;
+
+ ///
+ /// A snapshot copy of the current schedule list
+ ///
+ List> Schedule { get; }
+
+ ///
+ /// A snapshot copy of the current worker queue, that is items that are scheduled, but waiting for execution
+ ///
+ List WorkerQueue { get; }
+
+ ///
+ /// Forces the scheduler to re-evaluate the order.
+ /// Call this method if something changes
+ ///
+ void Reschedule();
+}
\ No newline at end of file
diff --git a/Duplicati.Library.RestAPI/Abstractions/IWorkerThreadsManager.cs b/Duplicati.Library.RestAPI/Abstractions/IWorkerThreadsManager.cs
new file mode 100644
index 000000000..6e4aa3b15
--- /dev/null
+++ b/Duplicati.Library.RestAPI/Abstractions/IWorkerThreadsManager.cs
@@ -0,0 +1,15 @@
+#nullable enable
+using System;
+using Duplicati.Library.Utility;
+using Duplicati.Server;
+
+namespace Duplicati.Library.RestAPI.Abstractions;
+
+public interface IWorkerThreadsManager
+{
+ void Spawn(Action item);
+
+ Tuple? CurrentTask { get; }
+ WorkerThread? WorkerThread { get; }
+ void UpdateThrottleSpeeds();
+}
\ No newline at end of file
diff --git a/Duplicati.Library.RestAPI/Database/Connection.cs b/Duplicati.Library.RestAPI/Database/Connection.cs
index 4fd7b1f90..fb35401d7 100644
--- a/Duplicati.Library.RestAPI/Database/Connection.cs
+++ b/Duplicati.Library.RestAPI/Database/Connection.cs
@@ -496,7 +496,7 @@ namespace Duplicati.Server.Database
}
}
- FIXMEGlobal.IncrementLastDataUpdateID();
+ FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
}
@@ -598,7 +598,7 @@ namespace Duplicati.Server.Database
}
tr.Commit();
- FIXMEGlobal.IncrementLastDataUpdateID();
+ FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
}
}
@@ -611,7 +611,7 @@ namespace Duplicati.Server.Database
{
AddOrUpdateSchedule(item, tr);
tr.Commit();
- FIXMEGlobal.IncrementLastDataUpdateID();
+ FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
}
}
@@ -674,7 +674,7 @@ namespace Duplicati.Server.Database
}
}
- FIXMEGlobal.IncrementLastDataUpdateID();
+ FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
}
@@ -694,7 +694,7 @@ namespace Duplicati.Server.Database
lock(m_lock)
DeleteFromDb("Schedule", ID);
- FIXMEGlobal.IncrementLastDataUpdateID();
+ FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
}
@@ -782,7 +782,7 @@ namespace Duplicati.Server.Database
FIXMEGlobal.DataConnection.ApplicationSettings.UnackedWarning = notifications.Any(x => x.ID != id && x.Type == Duplicati.Server.Serialization.NotificationType.Warning);
}
- FIXMEGlobal.IncrementLastNotificationUpdateID();
+ FIXMEGlobal.NotificationUpdateService.IncrementLastNotificationUpdateId();
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
return true;
@@ -822,7 +822,7 @@ namespace Duplicati.Server.Database
FIXMEGlobal.DataConnection.ApplicationSettings.UnackedWarning = true;
}
- FIXMEGlobal.IncrementLastNotificationUpdateID();
+ FIXMEGlobal.NotificationUpdateService.IncrementLastNotificationUpdateId();
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
}
diff --git a/Duplicati.Library.RestAPI/Database/ServerSettings.cs b/Duplicati.Library.RestAPI/Database/ServerSettings.cs
index 92b02bcfa..7f6fe2194 100644
--- a/Duplicati.Library.RestAPI/Database/ServerSettings.cs
+++ b/Duplicati.Library.RestAPI/Database/ServerSettings.cs
@@ -27,7 +27,7 @@ namespace Duplicati.Server.Database
{
public class ServerSettings
{
- private static class CONST
+ public static class CONST
{
public const string STARTUP_DELAY = "startup-delay";
public const string DOWNLOAD_SPEED_LIMIT = "max-download-speed";
@@ -113,13 +113,13 @@ namespace Duplicati.Server.Database
Value = n.Value
}, Database.Connection.SERVER_SETTINGS_ID);
- FIXMEGlobal.IncrementLastDataUpdateID();
+ FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
// In case the usage reporter is enabled or disabled, refresh now
FIXMEGlobal.StartOrStopUsageReporter();
// If throttle options were changed, update now
- FIXMEGlobal.UpdateThrottleSpeeds();
+ FIXMEGlobal.WorkerThreadsManager.UpdateThrottleSpeeds();
}
public string StartupDelayDuration
diff --git a/Duplicati.Library.RestAPI/Duplicati.Library.RestAPI.csproj b/Duplicati.Library.RestAPI/Duplicati.Library.RestAPI.csproj
index e10690aa5..3e01d4bd5 100644
--- a/Duplicati.Library.RestAPI/Duplicati.Library.RestAPI.csproj
+++ b/Duplicati.Library.RestAPI/Duplicati.Library.RestAPI.csproj
@@ -17,6 +17,9 @@
+
+ ..\Executables\net8\Duplicati.Server\bin\Debug\net8.0\Duplicati.WebserverCore.dll
+
..\thirdparty\HttpServer\HttpServer.dll
@@ -32,4 +35,8 @@
+
+
+
+
diff --git a/Duplicati.Library.RestAPI/FIXMEGlobal.cs b/Duplicati.Library.RestAPI/FIXMEGlobal.cs
index 2a8fc3148..3cb688507 100644
--- a/Duplicati.Library.RestAPI/FIXMEGlobal.cs
+++ b/Duplicati.Library.RestAPI/FIXMEGlobal.cs
@@ -2,6 +2,11 @@
using Duplicati.Server;
using System;
using System.Collections.Generic;
+using Duplicati.Library.IO;
+using Duplicati.Library.RestAPI.Abstractions;
+using Duplicati.Library.Utility;
+using Duplicati.WebserverCore.Abstractions;
+using Microsoft.Extensions.DependencyInjection;
namespace Duplicati.Library.RestAPI
{
@@ -11,7 +16,8 @@ namespace Duplicati.Library.RestAPI
*/
public static class FIXMEGlobal
{
-
+ public static IServiceProvider Provider { get; set; }
+
///
/// This is the only access to the database
///
@@ -20,7 +26,7 @@ namespace Duplicati.Library.RestAPI
///
/// The controller interface for pause/resume and throttle options
///
- public static LiveControls LiveControl;
+ public static LiveControls LiveControl => Provider.GetRequiredService();
///
/// A delegate method for creating a copy of the current progress state
@@ -30,24 +36,24 @@ namespace Duplicati.Library.RestAPI
///
/// The status event signaler, used to control long polling of status updates
///
- public static readonly EventPollNotify StatusEventNotifyer = new EventPollNotify();
+ public static EventPollNotify StatusEventNotifyer => Provider.GetRequiredService();
+
+ ///
+ /// For keeping and incrementing last last events Ids of db save and last notification
+ ///
+ public static INotificationUpdateService NotificationUpdateService => Provider.GetRequiredService();
///
/// This is the working thread
///
- public static Duplicati.Library.Utility.WorkerThread WorkThread;
+ public static WorkerThread WorkThread =>
+ Provider.GetRequiredService().WorkerThread;
- public static Func PeekLastDataUpdateID;
- public static Func PeekLastNotificationUpdateID;
-
- public static Action IncrementLastDataUpdateID;
-
- public static Action IncrementLastNotificationUpdateID;
+ public static IWorkerThreadsManager WorkerThreadsManager =>
+ Provider.GetRequiredService();
public static Action StartOrStopUsageReporter;
- public static Action UpdateThrottleSpeeds;
-
///
/// Gets the folder where Duplicati data is stored
///
@@ -56,19 +62,19 @@ namespace Duplicati.Library.RestAPI
///
/// This is the scheduling thread
///
- public static Scheduler Scheduler;
+ public static IScheduler Scheduler => Provider.GetRequiredService();
///
/// The log redirect handler
///
public static readonly LogWriteHandler LogHandler = new LogWriteHandler();
- public static Func, Server.Database.Connection> GetDatabaseConnection;
+ public static Func
- public class LiveControls
+ public class LiveControls : ILiveControls
{
///
/// The tag used for logging
@@ -86,6 +85,8 @@ namespace Duplicati.Server
///
public LiveControlState State { get { return m_state; } }
+ public bool IsPaused => State == LiveControlState.Paused;
+
///
/// The internal variable that tracks the the priority
///
@@ -160,7 +161,7 @@ namespace Duplicati.Server
///
/// The timer that is activated after a pause period.
///
- private readonly System.Threading.Timer m_waitTimer;
+ private System.Threading.Timer m_waitTimer;
///
/// The time that the current pause is expected to expire
@@ -170,7 +171,14 @@ namespace Duplicati.Server
///
/// Constructs a new instance of the LiveControl
///
- public LiveControls(Database.ServerSettings settings)
+ public LiveControls()
+ {
+ }
+
+ ///
+ /// Constructs a new instance of the LiveControl
+ ///
+ public void Init(Database.ServerSettings settings)
{
m_state = LiveControlState.Running;
m_waitTimer = new System.Threading.Timer(m_waitTimer_Tick, this, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
diff --git a/Duplicati.Library.RestAPI/NotificationUpdateService.cs b/Duplicati.Library.RestAPI/NotificationUpdateService.cs
new file mode 100644
index 000000000..2bbec0ce3
--- /dev/null
+++ b/Duplicati.Library.RestAPI/NotificationUpdateService.cs
@@ -0,0 +1,50 @@
+namespace Duplicati.Library.RestAPI;
+
+public interface INotificationUpdateService
+{
+ ///
+ /// An event ID that increases whenever the database is updated
+ ///
+ long LastDataUpdateId { get; }
+
+ ///
+ /// An event ID that increases whenever a notification is updated
+ ///
+ long LastNotificationUpdateId { get; }
+
+ void IncrementLastDataUpdateId();
+ void IncrementLastNotificationUpdateId();
+}
+
+public class NotificationUpdateService : INotificationUpdateService
+{
+ ///
+ /// An event ID that increases whenever the database is updated
+ ///
+ public long LastDataUpdateId { get; private set; } = 0;
+
+ private readonly object _lastDataUpdateIdLock = new();
+
+ ///
+ /// An event ID that increases whenever a notification is updated
+ ///
+ public long LastNotificationUpdateId { get; private set; } = 0;
+
+ private readonly object _lastNotificationUpdateIdLock = new();
+
+ public void IncrementLastDataUpdateId()
+ {
+ lock (_lastDataUpdateIdLock)
+ {
+ LastDataUpdateId++;
+ }
+ }
+
+ public void IncrementLastNotificationUpdateId()
+ {
+ lock (_lastNotificationUpdateIdLock)
+ {
+ LastNotificationUpdateId++;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Duplicati.Library.RestAPI/RESTMethods/Backups.cs b/Duplicati.Library.RestAPI/RESTMethods/Backups.cs
index bae68fa57..2c06f365d 100644
--- a/Duplicati.Library.RestAPI/RESTMethods/Backups.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/Backups.cs
@@ -132,7 +132,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
Serializable.ImportExportStructure importedStructure = Backups.LoadConfiguration(configurationFile, importMetadata, getPassword);
// This will create the Duplicati-server.sqlite database file if it doesn't exist.
- using (Duplicati.Server.Database.Connection connection = FIXMEGlobal.GetDatabaseConnection(advancedOptions))
+ using (Duplicati.Server.Database.Connection connection = FIXMEGlobal.GetDatabaseConnection(null, advancedOptions))
{
if (connection.Backups.Any(x => x.Name.Equals(importedStructure.Backup.Name, StringComparison.OrdinalIgnoreCase)))
{
diff --git a/Duplicati.Library.RestAPI/RESTMethods/RequestInfo.cs b/Duplicati.Library.RestAPI/RESTMethods/RequestInfo.cs
index 2069b6cf9..f0c511ef4 100644
--- a/Duplicati.Library.RestAPI/RESTMethods/RequestInfo.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/RequestInfo.cs
@@ -24,8 +24,8 @@ namespace Duplicati.Server.WebServer.RESTMethods
{
public class RequestInfo : IDisposable
{
- public HttpServer.IHttpRequest Request { get; private set; }
- public HttpServer.IHttpResponse Response { get; private set; }
+ public HttpServer.IHttpRequest Request { get; }
+ public HttpServer.IHttpResponse Response { get; }
public HttpServer.Sessions.IHttpSession Session { get; private set; }
public BodyWriter BodyWriter { get; private set; }
public RequestInfo(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
diff --git a/Duplicati.Library.RestAPI/RESTMethods/ServerState.cs b/Duplicati.Library.RestAPI/RESTMethods/ServerState.cs
index bd1fa914a..70c382b1d 100644
--- a/Duplicati.Library.RestAPI/RESTMethods/ServerState.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/ServerState.cs
@@ -24,7 +24,7 @@ using System.Collections.Generic;
namespace Duplicati.Server.WebServer.RESTMethods
{
- public class ServerState : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented
+ public class ServerState : IRESTMethodPOST, IRESTMethodDocumented
{
public void GET(string key, RequestInfo info)
{
@@ -32,16 +32,17 @@ namespace Duplicati.Server.WebServer.RESTMethods
long id = 0;
long.TryParse(key, out id);
+ var serverStatus = new Serializable.ServerStatus();
if (info.LongPollCheck(FIXMEGlobal.StatusEventNotifyer, ref id, out isError))
{
//Make sure we do not report a higher number than the eventnotifier says
- var st = new Serializable.ServerStatus();
+ var st = serverStatus;
st.LastEventID = id;
info.OutputOK(st);
}
else if (!isError)
{
- info.OutputOK(new Serializable.ServerStatus());
+ info.OutputOK(serverStatus);
}
}
diff --git a/Duplicati.Library.RestAPI/Runner.cs b/Duplicati.Library.RestAPI/Runner.cs
index 1dcdc267c..76554701b 100644
--- a/Duplicati.Library.RestAPI/Runner.cs
+++ b/Duplicati.Library.RestAPI/Runner.cs
@@ -694,7 +694,7 @@ namespace Duplicati.Server
if (ex is UserInformationException exception)
messageid = exception.HelpID;
- FIXMEGlobal.IncrementLastDataUpdateID();
+ FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
FIXMEGlobal.DataConnection.RegisterNotification(
NotificationType.Error,
backup.IsTemporary ?
@@ -876,7 +876,7 @@ namespace Duplicati.Server
if (!backup.IsTemporary)
FIXMEGlobal.DataConnection.SetMetadata(backup.Metadata, long.Parse(backup.ID), null);
- FIXMEGlobal.IncrementLastDataUpdateID();
+ FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
}
diff --git a/Duplicati.Library.RestAPI/Scheduler.cs b/Duplicati.Library.RestAPI/Scheduler.cs
index 81a8cdf41..7544bb7c2 100644
--- a/Duplicati.Library.RestAPI/Scheduler.cs
+++ b/Duplicati.Library.RestAPI/Scheduler.cs
@@ -1,4 +1,5 @@
#region Disclaimer / License
+
// Copyright (C) 2015, The Duplicati Team
// http://www.duplicati.com, info@duplicati.com
//
@@ -18,8 +19,8 @@
//
using Duplicati.Server.Serialization.Interface;
-
#endregion
+
using System;
using System.Collections.Generic;
using System.Text;
@@ -27,32 +28,37 @@ using System.Linq;
using System.Threading;
using Duplicati.Library.Utility;
using Duplicati.Library.RestAPI;
+using Duplicati.WebserverCore.Abstractions;
namespace Duplicati.Server
{
///
/// This class handles scheduled runs of backups
///
- public class Scheduler
+ public class Scheduler : IScheduler
{
private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType();
///
/// The thread that runs the scheduler
///
- private readonly Thread m_thread;
+ private Thread m_thread;
+
///
/// A termination flag
///
private volatile bool m_terminate;
+
///
/// The worker thread that is invoked to do work
///
- private readonly WorkerThread m_worker;
+ private WorkerThread m_worker;
+
///
/// The wait event
///
- private readonly AutoResetEvent m_event;
+ private AutoResetEvent m_event;
+
///
/// The data synchronization lock
///
@@ -67,17 +73,24 @@ namespace Duplicati.Server
/// The currently scheduled items
///
private KeyValuePair[] m_schedule;
-
+
///
/// List of update tasks, used to set the timestamp on the schedule once completed
///
- private readonly Dictionary> m_updateTasks;
+ private Dictionary> m_updateTasks;
///
/// Constructs a new scheduler
///
+ public Scheduler()
+ {
+ }
+
+ ///
+ /// Initializes scheduler
+ ///
/// The worker thread
- public Scheduler(WorkerThread worker)
+ public void Init(WorkerThread worker)
{
m_thread = new Thread(new ThreadStart(Runner));
m_worker = worker;
@@ -92,6 +105,13 @@ namespace Duplicati.Server
m_thread.Start();
}
+ public IList> GetSchedulerQueueIds()
+ {
+ return (from n in WorkerQueue
+ where n.Backup != null
+ select new Tuple(n.TaskID, n.Backup.ID)).ToList();
+ }
+
///
/// Forces the scheduler to re-evaluate the order.
/// Call this method if something changes
@@ -104,13 +124,13 @@ namespace Duplicati.Server
///
/// A snapshot copy of the current schedule list
///
- public List> Schedule
- {
- get
+ public List> Schedule
+ {
+ get
{
lock (m_lock)
return m_schedule.ToList();
- }
+ }
}
///
@@ -118,10 +138,7 @@ namespace Duplicati.Server
///
public List WorkerQueue
{
- get
- {
- return (from t in m_worker.CurrentTasks where t != null select t).ToList();
- }
+ get { return (from t in m_worker.CurrentTasks where t != null select t).ToList(); }
}
///
@@ -135,8 +152,13 @@ namespace Duplicati.Server
if (wait)
{
- try { m_thread.Join(); }
- catch { }
+ try
+ {
+ m_thread.Join();
+ }
+ catch
+ {
+ }
}
}
@@ -148,7 +170,8 @@ namespace Duplicati.Server
/// The repetition interval
/// The days the backup is allowed to run
/// The next valid date, or throws an exception if no such date can be found
- public static DateTime GetNextValidTime(DateTime basetime, DateTime firstdate, string repetition, DayOfWeek[] allowedDays)
+ public static DateTime GetNextValidTime(DateTime basetime, DateTime firstdate, string repetition,
+ DayOfWeek[] allowedDays)
{
var res = basetime;
@@ -195,26 +218,25 @@ namespace Duplicati.Server
throw new Exception(Strings.Scheduler.InvalidTimeSetupError(basetime, repetition, sb.ToString()));
}
-
+
return res;
}
-
+
private void OnCompleted(WorkerThread worker, Runner.IRunnerData task)
{
Tuple t = null;
- lock(m_lock)
+ lock (m_lock)
{
if (task != null && m_updateTasks.TryGetValue(task, out t))
m_updateTasks.Remove(task);
}
-
+
if (t != null)
{
t.Item1.Time = t.Item2;
t.Item1.LastRun = t.Item3;
FIXMEGlobal.DataConnection.AddOrUpdateSchedule(t.Item1);
}
-
}
private void OnStartingWork(WorkerThread worker, Runner.IRunnerData task)
@@ -223,8 +245,8 @@ namespace Duplicati.Server
{
return;
}
-
- lock(m_lock)
+
+ lock (m_lock)
{
if (m_updateTasks.TryGetValue(task, out Tuple scheduleInfo))
{
@@ -245,10 +267,10 @@ namespace Duplicati.Server
{
//TODO: As this is executed repeatedly we should cache it
// to avoid frequent db lookups
-
+
//Determine schedule list
var lst = FIXMEGlobal.DataConnection.Schedules;
- foreach(var sc in lst)
+ foreach (var sc in lst)
{
if (!string.IsNullOrEmpty(sc.Repeat))
{
@@ -267,7 +289,7 @@ namespace Duplicati.Server
{
start = startkey.Value;
}
-
+
try
{
// Recover from timedrift issues by overriding the dates if the last run date is in the future.
@@ -276,11 +298,13 @@ namespace Duplicati.Server
start = DateTime.UtcNow;
last = DateTime.UtcNow;
}
+
start = GetNextValidTime(start, last, sc.Repeat, sc.AllowedDays);
}
catch (Exception ex)
{
- FIXMEGlobal.DataConnection.LogError(sc.ID.ToString(), "Scheduler failed to find next date", ex);
+ FIXMEGlobal.DataConnection.LogError(sc.ID.ToString(), "Scheduler failed to find next date",
+ ex);
}
//If time is exceeded, run it now
@@ -288,15 +312,17 @@ namespace Duplicati.Server
{
var jobsToRun = new List();
//TODO: Cache this to avoid frequent lookups
- foreach(var id in FIXMEGlobal.DataConnection.GetBackupIDsForTags(sc.Tags).Distinct().Select(x => x.ToString()))
+ foreach (var id in FIXMEGlobal.DataConnection.GetBackupIDsForTags(sc.Tags).Distinct()
+ .Select(x => x.ToString()))
{
//See if it is already queued
var tmplst = from n in m_worker.CurrentTasks
- where n.Operation == Duplicati.Server.Serialization.DuplicatiOperation.Backup
- select n.Backup;
+ where n.Operation == Duplicati.Server.Serialization.DuplicatiOperation.Backup
+ select n.Backup;
var tastTemp = m_worker.CurrentTask;
- if (tastTemp != null && tastTemp.Operation == Duplicati.Server.Serialization.DuplicatiOperation.Backup)
- tmplst = tmplst.Union(new [] { tastTemp.Backup });
+ if (tastTemp != null && tastTemp.Operation ==
+ Duplicati.Server.Serialization.DuplicatiOperation.Backup)
+ tmplst = tmplst.Union(new[] { tastTemp.Backup });
//If it is not already in queue, put it there
if (!tmplst.Any(x => x.ID == id))
@@ -306,13 +332,18 @@ namespace Duplicati.Server
{
Dictionary options = Duplicati.Server.Runner.GetCommonOptions();
Duplicati.Server.Runner.ApplyOptions(entry, options);
- if ((new Duplicati.Library.Main.Options(options)).DisableOnBattery && (Duplicati.Library.Utility.Power.PowerSupply.GetSource() == Duplicati.Library.Utility.Power.PowerSupply.Source.Battery))
+ if ((new Duplicati.Library.Main.Options(options)).DisableOnBattery &&
+ (Duplicati.Library.Utility.Power.PowerSupply.GetSource() ==
+ Duplicati.Library.Utility.Power.PowerSupply.Source.Battery))
{
- Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "BackupDisabledOnBattery", "Scheduled backup disabled while on battery power.");
+ Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG,
+ "BackupDisabledOnBattery",
+ "Scheduled backup disabled while on battery power.");
}
else
{
- jobsToRun.Add(Server.Runner.CreateTask(Duplicati.Server.Serialization.DuplicatiOperation.Backup, entry));
+ jobsToRun.Add(Server.Runner.CreateTask(
+ Duplicati.Server.Serialization.DuplicatiOperation.Backup, entry));
}
}
}
@@ -321,27 +352,32 @@ namespace Duplicati.Server
// Calculate next time, by finding the first entry later than now
try
{
- start = GetNextValidTime(start, new DateTime(Math.Max(DateTime.UtcNow.AddSeconds(1).Ticks, start.AddSeconds(1).Ticks), DateTimeKind.Utc), sc.Repeat, sc.AllowedDays);
+ start = GetNextValidTime(start,
+ new DateTime(
+ Math.Max(DateTime.UtcNow.AddSeconds(1).Ticks, start.AddSeconds(1).Ticks),
+ DateTimeKind.Utc), sc.Repeat, sc.AllowedDays);
}
- catch(Exception ex)
+ catch (Exception ex)
{
- FIXMEGlobal.DataConnection.LogError(sc.ID.ToString(), "Scheduler failed to find next date", ex);
+ FIXMEGlobal.DataConnection.LogError(sc.ID.ToString(),
+ "Scheduler failed to find next date", ex);
continue;
}
-
+
Server.Runner.IRunnerData lastJob = jobsToRun.LastOrDefault();
if (lastJob != null)
{
lock (m_lock)
{
// The actual last run time will be updated when the StartingWork event is raised.
- m_updateTasks[lastJob] = new Tuple(sc, start, DateTime.UtcNow);
+ m_updateTasks[lastJob] =
+ new Tuple(sc, start, DateTime.UtcNow);
}
}
foreach (var job in jobsToRun)
m_worker.AddTask(job);
-
+
if (start < DateTime.UtcNow)
{
//TODO: Report this somehow
@@ -349,20 +385,20 @@ namespace Duplicati.Server
}
}
- scheduled[sc.ID] = new KeyValuePair(scticks, start);
+ scheduled[sc.ID] = new KeyValuePair(scticks, start);
}
}
var existing = lst.ToDictionary(x => x.ID);
//Sort them, lock as we assign the m_schedule variable
- lock(m_lock)
+ lock (m_lock)
m_schedule = (from n in scheduled
where existing.ContainsKey(n.Key)
orderby n.Value.Value
select new KeyValuePair(n.Value.Value, existing[n.Key])).ToArray();
// Remove unused entries
- foreach(var c in (from n in scheduled where !existing.ContainsKey(n.Key) select n.Key).ToArray())
+ foreach (var c in (from n in scheduled where !existing.ContainsKey(n.Key) select n.Key).ToArray())
scheduled.Remove(c);
//Raise event if needed
@@ -406,8 +442,7 @@ namespace Duplicati.Server
if (allowedDays == null || allowedDays.Length == 0)
return true;
else
- return Array.IndexOf(allowedDays, localTime.DayOfWeek) >= 0;
+ return Array.IndexOf(allowedDays, localTime.DayOfWeek) >= 0;
}
-
}
-}
+}
\ No newline at end of file
diff --git a/Duplicati.Library.RestAPI/Serializable/ServerStatus.cs b/Duplicati.Library.RestAPI/Serializable/ServerStatus.cs
index c77ae6027..6f0843018 100644
--- a/Duplicati.Library.RestAPI/Serializable/ServerStatus.cs
+++ b/Duplicati.Library.RestAPI/Serializable/ServerStatus.cs
@@ -139,10 +139,9 @@ namespace Duplicati.Server.Serializable
set { m_lastEventID = value; }
}
- public long LastDataUpdateID { get { return FIXMEGlobal.PeekLastDataUpdateID(); } }
-
- public long LastNotificationUpdateID { get { return FIXMEGlobal.PeekLastNotificationUpdateID(); } }
+ public long LastDataUpdateID => FIXMEGlobal.NotificationUpdateService.LastDataUpdateId;
+ public long LastNotificationUpdateID => FIXMEGlobal.NotificationUpdateService.LastNotificationUpdateId;
}
}
diff --git a/Duplicati.Library.RestAPI/UpdatePollThread.cs b/Duplicati.Library.RestAPI/UpdatePollThread.cs
index c82bd8d2d..869f992d0 100644
--- a/Duplicati.Library.RestAPI/UpdatePollThread.cs
+++ b/Duplicati.Library.RestAPI/UpdatePollThread.cs
@@ -28,12 +28,12 @@ namespace Duplicati.Server
///
public class UpdatePollThread
{
- private readonly Thread m_thread;
+ private Thread m_thread;
private volatile bool m_terminated = false;
private volatile bool m_download = false;
private volatile bool m_forceCheck = false;
private readonly object m_lock = new object();
- private readonly AutoResetEvent m_waitSignal;
+ private AutoResetEvent m_waitSignal;
private double m_downloadProgress;
public bool IsUpdateRequested { get; private set; } = false;
@@ -51,14 +51,16 @@ namespace Duplicati.Server
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
}
}
-
- public UpdatePollThread()
+
+ public void Init()
{
m_waitSignal = new AutoResetEvent(false);
ThreadState = UpdatePollerStates.Waiting;
- m_thread = new Thread(Run);
- m_thread.IsBackground = true;
- m_thread.Name = "UpdatePollThread";
+ m_thread = new Thread(Run)
+ {
+ IsBackground = true,
+ Name = "UpdatePollThread"
+ };
m_thread.Start();
}
diff --git a/Duplicati.Library.RestAPI/WorkerThreadsManager.cs b/Duplicati.Library.RestAPI/WorkerThreadsManager.cs
new file mode 100644
index 000000000..79caca695
--- /dev/null
+++ b/Duplicati.Library.RestAPI/WorkerThreadsManager.cs
@@ -0,0 +1,34 @@
+#nullable enable
+using System;
+using Duplicati.Library.IO;
+using Duplicati.Library.RestAPI.Abstractions;
+using Duplicati.Library.Utility;
+using Duplicati.Server;
+using Duplicati.WebserverCore.Abstractions;
+
+namespace Duplicati.Library.RestAPI;
+
+public class WorkerThreadsManager(ILiveControls liveControls, IScheduler scheduler) : IWorkerThreadsManager
+{
+ public WorkerThread? WorkerThread { get; private set; }
+
+ public void Spawn(Action item)
+ {
+ WorkerThread = new WorkerThread(item, liveControls.IsPaused);
+ scheduler.Init(WorkerThread);
+ }
+
+ public Tuple? CurrentTask
+ {
+ get
+ {
+ var t = WorkerThread?.CurrentTask;
+ return t == null ? null : new Tuple(t.TaskID, t.Backup.ID);
+ }
+ }
+
+ public void UpdateThrottleSpeeds()
+ {
+ WorkerThread?.CurrentTask?.UpdateThrottleSpeed();
+ }
+}
\ No newline at end of file
diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs
index 857abfeba..db69443ad 100644
--- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs
+++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs
@@ -140,7 +140,7 @@ namespace Duplicati.GUI.TrayIcon
}
else if (Library.Utility.Utility.ParseBoolOption(options, READCONFIGFROMDB_OPTION))
{
- databaseConnection = Server.Program.GetDatabaseConnection(options);
+ databaseConnection = Server.Program.GetDatabaseConnection(null, options);
if (databaseConnection != null)
{
diff --git a/Duplicati/Library/AutoUpdater/IUpdateManagerAccessor.cs b/Duplicati/Library/AutoUpdater/IUpdateManagerAccessor.cs
new file mode 100644
index 000000000..26763743a
--- /dev/null
+++ b/Duplicati/Library/AutoUpdater/IUpdateManagerAccessor.cs
@@ -0,0 +1,6 @@
+namespace Duplicati.Library.AutoUpdater;
+
+public interface IUpdateManagerAccessor
+{
+ bool HasUpdateInstalled { get; }
+}
\ No newline at end of file
diff --git a/Duplicati/Library/AutoUpdater/UpdateManagerAccessor.cs b/Duplicati/Library/AutoUpdater/UpdateManagerAccessor.cs
new file mode 100644
index 000000000..cc1cae43c
--- /dev/null
+++ b/Duplicati/Library/AutoUpdater/UpdateManagerAccessor.cs
@@ -0,0 +1,6 @@
+namespace Duplicati.Library.AutoUpdater;
+
+public class UpdateManagerAccessor : IUpdateManagerAccessor
+{
+ public bool HasUpdateInstalled => UpdaterManager.HasUpdateInstalled;
+}
diff --git a/Duplicati/Library/Common/ILiveControls.cs b/Duplicati/Library/Common/ILiveControls.cs
new file mode 100644
index 000000000..cef107b11
--- /dev/null
+++ b/Duplicati/Library/Common/ILiveControls.cs
@@ -0,0 +1,6 @@
+namespace Duplicati.Library.IO;
+
+public interface ILiveControls
+{
+ bool IsPaused { get; }
+}
\ No newline at end of file
diff --git a/Duplicati/Library/Utility/Abstractions/IBoolParser.cs b/Duplicati/Library/Utility/Abstractions/IBoolParser.cs
new file mode 100644
index 000000000..782b80a76
--- /dev/null
+++ b/Duplicati/Library/Utility/Abstractions/IBoolParser.cs
@@ -0,0 +1,6 @@
+namespace Duplicati.Library.Utility.Abstractions;
+
+public interface IBoolParser
+{
+ bool ParseBool(string value, bool @default = false);
+}
\ No newline at end of file
diff --git a/Duplicati/Library/Utility/BoolParser.cs b/Duplicati/Library/Utility/BoolParser.cs
new file mode 100644
index 000000000..1f4f0d757
--- /dev/null
+++ b/Duplicati/Library/Utility/BoolParser.cs
@@ -0,0 +1,11 @@
+using Duplicati.Library.Utility.Abstractions;
+
+namespace Duplicati.Library.Utility;
+
+public class BoolParser : IBoolParser
+{
+ public bool ParseBool(string value, bool @default = false)
+ {
+ return Utility.ParseBool(value, @default);
+ }
+}
\ No newline at end of file
diff --git a/Duplicati/Library/Utility/WorkerThread.cs b/Duplicati/Library/Utility/WorkerThread.cs
index dfdd99f2a..267b40e89 100644
--- a/Duplicati/Library/Utility/WorkerThread.cs
+++ b/Duplicati/Library/Utility/WorkerThread.cs
@@ -1,23 +1,23 @@
-// Copyright (C) 2024, 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.
+// Copyright (C) 2024, 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;
using System.Collections.Generic;
@@ -391,4 +391,4 @@ namespace Duplicati.Library.Utility
return true;
}
}
-}
+}
\ No newline at end of file
diff --git a/Duplicati/Server/Duplicati.Server.Serialization/Serializer.cs b/Duplicati/Server/Duplicati.Server.Serialization/Serializer.cs
index 14c410ee7..79ab5492d 100644
--- a/Duplicati/Server/Duplicati.Server.Serialization/Serializer.cs
+++ b/Duplicati/Server/Duplicati.Server.Serialization/Serializer.cs
@@ -1,23 +1,23 @@
-// Copyright (C) 2024, 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.
+// Copyright (C) 2024, 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;
using System.Collections.Generic;
using System.IO;
@@ -31,26 +31,28 @@ namespace Duplicati.Server.Serialization
{
public class Serializer
{
- protected static readonly JsonSerializerSettings m_jsonSettings;
+ public static JsonSerializerSettings JsonSettings { get; }
protected static readonly Formatting m_jsonFormatting = Formatting.Indented;
static Serializer()
{
- m_jsonSettings = new JsonSerializerSettings();
- m_jsonSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
- m_jsonSettings.Converters = new JsonConverter[] {
- new DayOfWeekConcerter(),
- new StringEnumConverter(),
- new SerializableStatusCreator(),
- new SettingsCreator(),
- new FilterCreator(),
- new NotificationCreator(),
- }.ToList();
+ JsonSettings = new JsonSerializerSettings
+ {
+ ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
+ Converters = new JsonConverter[] {
+ new DayOfWeekConcerter(),
+ new StringEnumConverter(),
+ new SerializableStatusCreator(),
+ new SettingsCreator(),
+ new FilterCreator(),
+ new NotificationCreator(),
+ }.ToList()
+ };
}
public static void SerializeJson(System.IO.TextWriter sw, object o, bool preventDispose = false)
{
- Newtonsoft.Json.JsonSerializer jsonSerializer = Newtonsoft.Json.JsonSerializer.Create(m_jsonSettings);
+ Newtonsoft.Json.JsonSerializer jsonSerializer = Newtonsoft.Json.JsonSerializer.Create(JsonSettings);
var jsonWriter = new JsonTextWriter(sw);
using (preventDispose ? null : jsonWriter)
{
@@ -63,7 +65,7 @@ namespace Duplicati.Server.Serialization
public static async Task SerializeJsonAsync(System.IO.TextWriter tw, object o, bool preventDispose = false)
{
- Newtonsoft.Json.JsonSerializer jsonSerializer = Newtonsoft.Json.JsonSerializer.Create(m_jsonSettings);
+ Newtonsoft.Json.JsonSerializer jsonSerializer = Newtonsoft.Json.JsonSerializer.Create(JsonSettings);
StringBuilder sb = new StringBuilder();
StringWriter sw = new StringWriter(sb);
var jsonWriter = new JsonTextWriter(sw);
@@ -80,7 +82,7 @@ namespace Duplicati.Server.Serialization
public static T Deserialize(System.IO.TextReader sr)
{
- Newtonsoft.Json.JsonSerializer jsonSerializer = Newtonsoft.Json.JsonSerializer.Create(m_jsonSettings);
+ Newtonsoft.Json.JsonSerializer jsonSerializer = Newtonsoft.Json.JsonSerializer.Create(JsonSettings);
using (var jsonReader = new JsonTextReader(sr))
{
jsonReader.Culture = System.Globalization.CultureInfo.InvariantCulture;
diff --git a/Duplicati/Server/Program.cs b/Duplicati/Server/Program.cs
index 82dcf0db8..28b2ef77b 100644
--- a/Duplicati/Server/Program.cs
+++ b/Duplicati/Server/Program.cs
@@ -24,8 +24,13 @@ using System.Globalization;
using System.Linq;
using Duplicati.Library.Common;
using Duplicati.Library.Common.IO;
+using Duplicati.Library.IO;
using Duplicati.Library.RestAPI;
using Duplicati.WebserverCore;
+using Duplicati.WebserverCore.Abstractions;
+using Duplicati.WebserverCore.Database;
+using Microsoft.Extensions.DependencyInjection;
+using Sharp.Xmpp.Extensions.Dataforms;
namespace Duplicati.Server
{
@@ -78,12 +83,7 @@ namespace Duplicati.Server
///
/// This is the scheduling thread
///
- public static Scheduler Scheduler { get => FIXMEGlobal.Scheduler; set => FIXMEGlobal.Scheduler = value; }
-
- ///
- /// This is the working thread
- ///
- public static Duplicati.Library.Utility.WorkerThread WorkThread { get => FIXMEGlobal.WorkThread; set => FIXMEGlobal.WorkThread = value; }
+ public static IScheduler Scheduler { get => FIXMEGlobal.Scheduler; }
///
/// List of completed task results
@@ -108,7 +108,7 @@ namespace Duplicati.Server
///
/// The controller interface for pause/resume and throttle options
///
- public static LiveControls LiveControl { get => FIXMEGlobal.LiveControl; set => FIXMEGlobal.LiveControl = value; }
+ public static LiveControls LiveControl { get => DuplicatiWebserver.Provider.GetRequiredService() ; }
///
/// The application exit event
@@ -120,15 +120,23 @@ namespace Duplicati.Server
///
private static WebServer.Server WebServer;
+ ///
+ /// Duplicati webserver instance
+ ///
+ public static DuplicatiWebserver DuplicatiWebserver { get; set; }
+
///
/// Callback to shutdown the modern webserver
///
- private static Action ShutdownModernWebserver;
+ private static void ShutdownModernWebserver()
+ {
+ DuplicatiWebserver.Stop().GetAwaiter().GetResult();
+ }
///
/// The update poll thread.
///
- public static UpdatePollThread UpdatePoller { get => FIXMEGlobal.UpdatePoller; set => FIXMEGlobal.UpdatePoller = value; }
+ public static UpdatePollThread UpdatePoller => FIXMEGlobal.UpdatePoller;
///
/// An event that is set once the server is ready to respond to requests
@@ -138,23 +146,13 @@ namespace Duplicati.Server
///
/// The status event signaler, used to control long polling of status updates
///
- public static EventPollNotify StatusEventNotifyer { get => FIXMEGlobal.StatusEventNotifyer; }
+ public static EventPollNotify StatusEventNotifyer => FIXMEGlobal.Provider.GetRequiredService();
///
/// A delegate method for creating a copy of the current progress state
///
public static Func GenerateProgressState { get => FIXMEGlobal.GenerateProgressState; set => FIXMEGlobal.GenerateProgressState = value; }
- ///
- /// An event ID that increases whenever the database is updated
- ///
- public static long LastDataUpdateID = 0;
-
- ///
- /// An event ID that increases whenever a notification is updated
- ///
- public static long LastNotificationUpdateID = 0;
-
///
/// The log redirect handler
///
@@ -193,25 +191,10 @@ namespace Duplicati.Server
set { DataConnection.ApplicationSettings.ServerPortChanged = value; }
}
- public static void IncrementLastDataUpdateID()
- {
- System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID);
- }
-
- public static void IncrementLastNotificationUpdateID()
- {
- System.Threading.Interlocked.Increment(ref Program.LastNotificationUpdateID);
- }
-
static Program()
{
- FIXMEGlobal.IncrementLastDataUpdateID = Program.IncrementLastDataUpdateID;
- FIXMEGlobal.PeekLastDataUpdateID = () => Program.LastDataUpdateID;
- FIXMEGlobal.IncrementLastNotificationUpdateID = Program.IncrementLastNotificationUpdateID;
- FIXMEGlobal.PeekLastNotificationUpdateID = () => Program.LastNotificationUpdateID;
FIXMEGlobal.GetDatabaseConnection = Program.GetDatabaseConnection;
FIXMEGlobal.StartOrStopUsageReporter = Program.StartOrStopUsageReporter;
- FIXMEGlobal.UpdateThrottleSpeeds = Program.UpdateThrottleSpeeds;
}
///
@@ -220,6 +203,9 @@ namespace Duplicati.Server
[STAThread]
public static int Main(string[] args)
{
+ // var methodInfo = typeof(TemporaryIoCAccessor).Assembly.EntryPoint;
+ // var program = Activator.CreateInstance(methodInfo!.DeclaringType!);
+ // methodInfo.Invoke(program, [Array.Empty()]);
return Duplicati.Library.AutoUpdater.UpdaterManager.RunFromMostRecent(typeof(Program).GetMethod("RealMain"), args, Duplicati.Library.AutoUpdater.AutoUpdateStrategy.Never);
}
@@ -278,8 +264,10 @@ namespace Duplicati.Server
try
{
-
- DataConnection = GetDatabaseConnection(commandlineOptions);
+ DuplicatiWebserver = new DuplicatiWebserver();
+ DuplicatiWebserver.InitWebServer();
+ FIXMEGlobal.Provider = DuplicatiWebserver.Provider;
+ DataConnection = GetDatabaseConnection(DuplicatiWebserver, commandlineOptions);
if (!DataConnection.ApplicationSettings.FixedInvalidBackupId)
DataConnection.FixInvalidBackupId();
@@ -292,12 +280,12 @@ namespace Duplicati.Server
ApplicationExitEvent = new System.Threading.ManualResetEvent(false);
- Library.AutoUpdater.UpdaterManager.OnError += (Exception obj) =>
+ Library.AutoUpdater.UpdaterManager.OnError += obj =>
{
DataConnection.LogError(null, "Error in updater", obj);
};
- UpdatePoller = new UpdatePollThread();
+ UpdatePoller.Init();
SetPurgeTempFilesTimer(commandlineOptions);
@@ -305,7 +293,7 @@ namespace Duplicati.Server
SetWorkerThread();
- StartWebServer(commandlineOptions);
+ StartWebServer(DuplicatiWebserver, commandlineOptions);
if (Library.Utility.Utility.ParseBoolOption(commandlineOptions, "ping-pong-keepalive"))
{
@@ -342,7 +330,7 @@ namespace Duplicati.Server
ShutdownModernWebserver();
UpdatePoller?.Terminate();
Scheduler?.Terminate(true);
- WorkThread?.Terminate(true);
+ FIXMEGlobal.WorkThread?.Terminate(true);
ApplicationInstance?.Dispose();
PurgeTempFilesTimer?.Dispose();
@@ -360,37 +348,33 @@ namespace Duplicati.Server
return 0;
}
- private static void StartWebServer(Dictionary commandlineOptions)
+ private static void StartWebServer(DuplicatiWebserver webserver, Dictionary commandlineOptions)
{
WebServer = new WebServer.Server(commandlineOptions);
ServerPortChanged |= WebServer.Port != DataConnection.ApplicationSettings.LastWebserverPort;
DataConnection.ApplicationSettings.LastWebserverPort = WebServer.Port;
-
- var server = new DuplicatiWebserver();
- ShutdownModernWebserver = server.Foo();
+
+ webserver.Start().GetAwaiter().GetResult();
}
private static void SetWorkerThread()
{
- WorkThread = new Duplicati.Library.Utility.WorkerThread((x) => { Runner.Run(x, true); },
- LiveControl.State == LiveControls.LiveControlState.Paused);
- Scheduler = new Scheduler(WorkThread);
-
- WorkThread.StartingWork += (worker, task) => { SignalNewEvent(null, null); };
- WorkThread.CompletedWork += (worker, task) => { SignalNewEvent(null, null); };
- WorkThread.WorkQueueChanged += (worker) => { SignalNewEvent(null, null); };
- Scheduler.NewSchedule += new EventHandler(SignalNewEvent);
- WorkThread.OnError += (worker, task, exception) =>
+ FIXMEGlobal.WorkerThreadsManager.Spawn(x => { Runner.Run(x, true); });
+ FIXMEGlobal.WorkThread.StartingWork += (worker, task) => { SignalNewEvent(null, null); };
+ FIXMEGlobal.WorkThread.CompletedWork += (worker, task) => { SignalNewEvent(null, null); };
+ FIXMEGlobal.WorkThread.WorkQueueChanged += (worker) => { SignalNewEvent(null, null); };
+ FIXMEGlobal.Scheduler.NewSchedule += new EventHandler(SignalNewEvent);
+ FIXMEGlobal.WorkThread.OnError += (worker, task, exception) =>
{
Program.DataConnection.LogError(task?.BackupID, "Error in worker", exception);
};
- var lastScheduleId = LastDataUpdateID;
+ var lastScheduleId = FIXMEGlobal.NotificationUpdateService.LastDataUpdateId;
Program.StatusEventNotifyer.NewEvent += (sender, e) =>
{
- if (lastScheduleId == LastDataUpdateID) return;
- lastScheduleId = LastDataUpdateID;
+ if (lastScheduleId == FIXMEGlobal.NotificationUpdateService.LastDataUpdateId) return;
+ lastScheduleId = FIXMEGlobal.NotificationUpdateService.LastDataUpdateId;
Program.Scheduler.Reschedule();
};
@@ -413,13 +397,13 @@ namespace Duplicati.Server
}
}
- Program.WorkThread.CompletedWork += (worker, task) => { RegisterTaskResult(task.TaskID, null); };
- Program.WorkThread.OnError += (worker, task, exception) => { RegisterTaskResult(task.TaskID, exception); };
+ FIXMEGlobal.WorkThread.CompletedWork += (worker, task) => { RegisterTaskResult(task.TaskID, null); };
+ FIXMEGlobal.WorkThread.OnError += (worker, task, exception) => { RegisterTaskResult(task.TaskID, exception); };
}
private static void SetLiveControls()
{
- LiveControl = new LiveControls(DataConnection.ApplicationSettings);
+ LiveControl.Init(DataConnection.ApplicationSettings);
LiveControl.StateChanged += LiveControl_StateChanged;
LiveControl.ThreadPriorityChanged += LiveControl_ThreadPriorityChanged;
LiveControl.ThrottleSpeedChanged += LiveControl_ThrottleSpeedChanged;
@@ -589,7 +573,7 @@ namespace Duplicati.Server
throw new Exception("Server invoked with --help");
}
- public static Database.Connection GetDatabaseConnection(Dictionary commandlineOptions)
+ public static Database.Connection GetDatabaseConnection(object webserver, Dictionary commandlineOptions)
{
var dbPassword = Environment.GetEnvironmentVariable(DB_KEY_ENV_NAME);
@@ -726,22 +710,11 @@ namespace Duplicati.Server
Library.UsageReporter.Reporter.SetReportLevel(reportLevel, disableUsageReporter);
}
- public static void UpdateThrottleSpeeds()
- {
- if (Program.WorkThread == null)
- return;
-
- var cur = Program.WorkThread.CurrentTask;
- if (cur != null)
- cur.UpdateThrottleSpeed();
- }
-
private static void SignalNewEvent(object sender, EventArgs e)
{
StatusEventNotifyer.SignalNewEvent();
}
-
///
/// Handles a change in the LiveControl and updates the Runner
///
@@ -765,29 +738,33 @@ namespace Duplicati.Server
///
/// This event handler updates the trayicon menu with the current state of the runner.
///
- static void LiveControl_StateChanged(object sender, EventArgs e)
+ ///
+ private static void LiveControl_StateChanged(object sender, EventArgs e)
{
+ var worker = FIXMEGlobal.WorkThread;
switch (LiveControl.State)
{
case LiveControls.LiveControlState.Paused:
{
- WorkThread.Pause();
- var t = WorkThread.CurrentTask;
+ worker.Pause();
+ var t = worker.CurrentTask;
t?.Pause();
break;
}
case LiveControls.LiveControlState.Running:
{
- WorkThread.Resume();
- var t = WorkThread.CurrentTask;
+ worker.Resume();
+ var t = worker.CurrentTask;
t?.Resume();
break;
}
+ default:
+ throw new InvalidOperationException($"State of {nameof(LiveControl)} was not recognized!");
}
StatusEventNotifyer.SignalNewEvent();
}
-
+
///
/// Simple method for tracking if the server has crashed
///
diff --git a/Duplicati/Server/webroot/ngax/scripts/services/ServerStatus.js b/Duplicati/Server/webroot/ngax/scripts/services/ServerStatus.js
index 8092dc04d..4ce10cd8c 100644
--- a/Duplicati/Server/webroot/ngax/scripts/services/ServerStatus.js
+++ b/Duplicati/Server/webroot/ngax/scripts/services/ServerStatus.js
@@ -1,6 +1,6 @@
backupApp.service('ServerStatus', function($rootScope, $timeout, AppService, AppUtils, gettextCatalog) {
- var longpolltime = 5 * 60 * 1000;
+ var longpolltime = 5 * 60 * 10000;
var waitingfortask = {};
@@ -112,7 +112,7 @@ backupApp.service('ServerStatus', function($rootScope, $timeout, AppService, App
var progressPollTimer = null;
var progressPollInProgress = false;
- var progressPollWait = 2000;
+ var progressPollWait = 20000;
function startUpdateProgressPoll() {
if (progressPollInProgress)
@@ -147,12 +147,13 @@ backupApp.service('ServerStatus', function($rootScope, $timeout, AppService, App
var longPollRetryTimer = null;
var countdownForForReLongPoll = function(m) {
+ console.log(arguments)
if (longPollRetryTimer != null) {
window.clearInterval(longPollRetryTimer);
longPollRetryTimer = null;
}
- var retryAt = new Date(new Date().getTime() + (state.xsfrerror ? 5000 : 15000));
+ var retryAt = new Date(new Date().getTime() + (state.xsfrerror ? 50000 : 150000));
state.connectionAttemptTimer = new Date() - retryAt;
$rootScope.$broadcast('serverstatechanged');
@@ -173,7 +174,7 @@ backupApp.service('ServerStatus', function($rootScope, $timeout, AppService, App
state.pauseTimeRemain = Math.max(0, AppUtils.parseDate(state.estimatedPauseEnd) - new Date());
if (state.pauseTimeRemain > 0 && updatepausetimer == null) {
- updatepausetimer = setInterval(pauseTimerUpdater, 500);
+ updatepausetimer = setInterval(pauseTimerUpdater, 50000);
} else if (state.pauseTimeRemain <= 0 && updatepausetimer != null) {
clearInterval(updatepausetimer);
updatepausetimer = null;
@@ -207,20 +208,20 @@ backupApp.service('ServerStatus', function($rootScope, $timeout, AppService, App
}
var url = '/serverstate/?lasteventid=' + parseInt(state.lastEventId) + '&longpoll=' + (((!fastcall) && (state.lastEventId > 0)) ? 'true' : 'false') + '&duration=' + parseInt((longpolltime-1000) / 1000) + 's';
- AppService.get(url, {timeout: state.lastEventId > 0 ? longpolltime : 5000}).then(
+ AppService.get(url, {timeout: state.lastEventId > 0 ? longpolltime : 50000}).then(
function (response) {
var oldEventId = state.lastEventId;
var anychanged =
- notifyIfChanged(response.data, 'LastEventID', 'lastEventId') |
- notifyIfChanged(response.data, 'LastDataUpdateID', 'lastDataUpdateId') |
- notifyIfChanged(response.data, 'LastNotificationUpdateID', 'lastNotificationUpdateId') |
- notifyIfChanged(response.data, 'ActiveTask', 'activeTask') |
- notifyIfChanged(response.data, 'ProgramState', 'programState') |
- notifyIfChanged(response.data, 'EstimatedPauseEnd', 'estimatedPauseEnd') |
- notifyIfChanged(response.data, 'UpdaterState', 'updaterState') |
- notifyIfChanged(response.data, 'UpdateReady', 'updateReady') |
- notifyIfChanged(response.data, 'UpdatedVersion', 'updatedVersion')|
- notifyIfChanged(response.data, 'UpdateDownloadProgress', 'updateDownloadProgress');
+ notifyIfChanged(response.data, 'lastEventID', 'lastEventId') |
+ notifyIfChanged(response.data, 'lastDataUpdateID', 'lastDataUpdateId') |
+ notifyIfChanged(response.data, 'lastNotificationUpdateID', 'lastNotificationUpdateId') |
+ notifyIfChanged(response.data, 'activeTask', 'activeTask') |
+ notifyIfChanged(response.data, 'programState', 'programState') |
+ notifyIfChanged(response.data, 'estimatedPauseEnd', 'estimatedPauseEnd') |
+ notifyIfChanged(response.data, 'updaterState', 'updaterState') |
+ notifyIfChanged(response.data, 'updateReady', 'updateReady') |
+ notifyIfChanged(response.data, 'updatedVersion', 'updatedVersion')|
+ notifyIfChanged(response.data, 'updateDownloadProgress', 'updateDownloadProgress');
if (!angular.equals(state.proposedSchedule, response.data.ProposedSchedule)) {
@@ -258,9 +259,6 @@ backupApp.service('ServerStatus', function($rootScope, $timeout, AppService, App
if (state.activeTask != null)
startUpdateProgressPoll();
-
-
- longpoll(false);
},
function(response) {
@@ -285,9 +283,6 @@ backupApp.service('ServerStatus', function($rootScope, $timeout, AppService, App
//If we got a new XSRF token this time, quickly retry
if (state.xsfrerror && !oldxsfrstate) {
longpoll(true);
- } else {
- // Otherwise, start countdown to next try
- countdownForForReLongPoll(function() { longpoll(true); });
}
}
@@ -298,7 +293,5 @@ backupApp.service('ServerStatus', function($rootScope, $timeout, AppService, App
);
};
- this.reconnect = function() { longpoll(true); };
-
longpoll(true);
});
diff --git a/Duplicati/UnitTest/ImportExportTests.cs b/Duplicati/UnitTest/ImportExportTests.cs
index d976ee7c8..0761b5fa2 100644
--- a/Duplicati/UnitTest/ImportExportTests.cs
+++ b/Duplicati/UnitTest/ImportExportTests.cs
@@ -105,7 +105,7 @@ namespace Duplicati.UnitTest
}
byte[] jsonByteArray;
- using (Program.DataConnection = Program.GetDatabaseConnection(advancedOptions))
+ using (Program.DataConnection = Program.GetDatabaseConnection(null, advancedOptions))
{
jsonByteArray = Server.WebServer.RESTMethods.Backup.ExportToJSON(backup, null);
}
@@ -127,7 +127,7 @@ namespace Duplicati.UnitTest
{
Dictionary metadata = new Dictionary {{"SourceFilesCount", "1"}};
Dictionary advancedOptions = new Dictionary {{"server-datafolder", this.serverDatafolder}};
- using (Program.DataConnection = Program.GetDatabaseConnection(advancedOptions))
+ using (Program.DataConnection = Program.GetDatabaseConnection(null, advancedOptions))
{
// Unencrypted file, don't import metadata.
string unencryptedWithoutMetadata = Path.Combine(this.serverDatafolder, Path.GetRandomFileName());
diff --git a/Duplicati/WebserverCore/Abstractions/FileEntry.cs b/Duplicati/WebserverCore/Abstractions/FileEntry.cs
new file mode 100644
index 000000000..320ff6842
--- /dev/null
+++ b/Duplicati/WebserverCore/Abstractions/FileEntry.cs
@@ -0,0 +1,10 @@
+namespace Duplicati.WebserverCore.Abstractions;
+
+public class FileEntry
+{
+ public string Path { get; set; } = "";
+ public string MD5 { get; set; } = "";
+ public string SHA256 { get; set; } = "";
+ public DateTime? LastWriteTime { get; set; }
+ public bool Ignore { get; set; }
+}
\ No newline at end of file
diff --git a/Duplicati/WebserverCore/Abstractions/ISettingsService.cs b/Duplicati/WebserverCore/Abstractions/ISettingsService.cs
new file mode 100644
index 000000000..06b07e0a2
--- /dev/null
+++ b/Duplicati/WebserverCore/Abstractions/ISettingsService.cs
@@ -0,0 +1,6 @@
+namespace Duplicati.WebserverCore.Abstractions;
+
+public interface ISettingsService
+{
+ ServerSettings GetSettings();
+}
\ No newline at end of file
diff --git a/Duplicati/WebserverCore/Abstractions/IStatusService.cs b/Duplicati/WebserverCore/Abstractions/IStatusService.cs
new file mode 100644
index 000000000..0bbe7c564
--- /dev/null
+++ b/Duplicati/WebserverCore/Abstractions/IStatusService.cs
@@ -0,0 +1,8 @@
+using Duplicati.WebserverCore.Dto;
+
+namespace Duplicati.WebserverCore.Abstractions;
+
+public interface IStatusService
+{
+ ServerStatusDto GetStatus();
+}
\ No newline at end of file
diff --git a/Duplicati/WebserverCore/Abstractions/IUpdateService.cs b/Duplicati/WebserverCore/Abstractions/IUpdateService.cs
new file mode 100644
index 000000000..d19d9486a
--- /dev/null
+++ b/Duplicati/WebserverCore/Abstractions/IUpdateService.cs
@@ -0,0 +1,6 @@
+namespace Duplicati.WebserverCore.Abstractions;
+
+public interface IUpdateService
+{
+ UpdateInfo? GetUpdateInfo();
+}
\ No newline at end of file
diff --git a/Duplicati/WebserverCore/Abstractions/IV1Endpoint.cs b/Duplicati/WebserverCore/Abstractions/IV1Endpoint.cs
new file mode 100644
index 000000000..057f5d4dc
--- /dev/null
+++ b/Duplicati/WebserverCore/Abstractions/IV1Endpoint.cs
@@ -0,0 +1,6 @@
+namespace Duplicati.WebserverCore.Abstractions;
+
+public interface IV1Endpoint
+{
+ public static abstract void Map(RouteGroupBuilder group);
+}
\ No newline at end of file
diff --git a/Duplicati/WebserverCore/Abstractions/Notifications/IWebsocketAccessor.cs b/Duplicati/WebserverCore/Abstractions/Notifications/IWebsocketAccessor.cs
new file mode 100644
index 000000000..56f801175
--- /dev/null
+++ b/Duplicati/WebserverCore/Abstractions/Notifications/IWebsocketAccessor.cs
@@ -0,0 +1,9 @@
+using System.Net.WebSockets;
+
+namespace Duplicati.WebserverCore.Abstractions.Notifications;
+
+public interface IWebsocketAccessor
+{
+ void AddConnection(WebSocket newConnection);
+ WebSocket[] OpenConnections { get; }
+}
\ No newline at end of file
diff --git a/Duplicati/WebserverCore/Abstractions/ServerSettings.cs b/Duplicati/WebserverCore/Abstractions/ServerSettings.cs
new file mode 100644
index 000000000..647788d98
--- /dev/null
+++ b/Duplicati/WebserverCore/Abstractions/ServerSettings.cs
@@ -0,0 +1,29 @@
+namespace Duplicati.WebserverCore.Abstractions;
+
+public class ServerSettings
+{
+ public string StartupDelay { get; set; } = "";
+ public string DownloadSpeedLimit { get; set; }= "";
+ public string UploadSpeedLimit { get; set; }= "";
+ public string ThreadPriority { get; set; }= "";
+ public string LastWebserverPort { get; set; }= "";
+ public string IsFirstRun { get; set; }= "";
+ public string ServerPortChanged { get; set; }= "";
+ public string ServerPassphrase { get; set; }= "";
+ public string ServerPassphraseSalt { get; set; }= "";
+ public string ServerPassphraseTrayIcon { get; set; }= "";
+ public string ServerPassphraseTrayIconHash { get; set; }= "";
+ public string UpdateCheckLast { get; set; }= "";
+ public string UpdateCheckInterval { get; set; }= "";
+ public string UpdateCheckNewVersion { get; set; }= "";
+ public bool UnackedError { get; set; }
+ public bool UnackedWarning { get; set; }
+ public string ServerListenInterface { get; set; }= "";
+ public string ServerSslCertificate { get; set; }= "";
+ public string HasFixedInvalidBackupId { get; set; }= "";
+ public string UpdateChannel { get; set; }= "";
+ public string UsageReporterLevel { get; set; }= "";
+ public string HasAskedForPasswordProtection { get; set; }= "";
+ public string DisableTrayIconLogin { get; set; }= "";
+ public string ServerAllowedHostnames { get; set; }= "";
+}
\ No newline at end of file
diff --git a/Duplicati/WebserverCore/Abstractions/UpdateInfo.cs b/Duplicati/WebserverCore/Abstractions/UpdateInfo.cs
new file mode 100644
index 000000000..b3554bf95
--- /dev/null
+++ b/Duplicati/WebserverCore/Abstractions/UpdateInfo.cs
@@ -0,0 +1,17 @@
+namespace Duplicati.WebserverCore.Abstractions;
+
+public class UpdateInfo
+{
+ public string Displayname { get; set; } = "";
+ public string Version { get; set; } = "";
+ public DateTime? ReleaseTime { get; set; }
+ public string ReleaseType { get; set; } = "";
+ public string UpdateSeverity { get; set; } = "";
+ public string ChangeInfo { get; set; } = "";
+ public long CompressedSize { get; set; }
+ public long UncompressedSize { get; set; }
+ public string SHA256 { get; set; } = "";
+ public string MD5 { get; set; } = "";
+ public string[] RemoteURLS { get; set; } = [];
+ public FileEntry[] Files { get; set; } = [];
+}
\ No newline at end of file
diff --git a/Duplicati/WebserverCore/ApplicationPartsLogger.cs b/Duplicati/WebserverCore/ApplicationPartsLogger.cs
new file mode 100644
index 000000000..7cd5f64c8
--- /dev/null
+++ b/Duplicati/WebserverCore/ApplicationPartsLogger.cs
@@ -0,0 +1,32 @@
+using Microsoft.AspNetCore.Mvc.ApplicationParts;
+using Microsoft.AspNetCore.Mvc.Controllers;
+
+namespace Duplicati.WebserverCore;
+
+//Useful for debugging ASP.net magically loading controllers
+public class ApplicationPartsLogger(ILogger logger, ApplicationPartManager partManager)
+ : IHostedService
+{
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ // Get the names of all the application parts. This is the short assembly name for AssemblyParts
+ var applicationParts = partManager.ApplicationParts.Select(x => x.Name);
+
+ // Create a controller feature, and populate it from the application parts
+ var controllerFeature = new ControllerFeature();
+ partManager.PopulateFeature(controllerFeature);
+
+ // Get the names of all of the controllers
+ var controllers = controllerFeature.Controllers.Select(x => x.Name);
+
+ // Log the application parts and controllers
+ logger.LogInformation(
+ "Found the following application parts: '{ApplicationParts}' with the following controllers: '{Controllers}'",
+ string.Join(", ", applicationParts), string.Join(", ", controllers));
+
+ return Task.CompletedTask;
+ }
+
+ // Required by the interface
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+}
\ No newline at end of file
diff --git a/Duplicati/WebserverCore/Database/Configuration/OptionConfiguration.cs b/Duplicati/WebserverCore/Database/Configuration/OptionConfiguration.cs
new file mode 100644
index 000000000..23a5deb77
--- /dev/null
+++ b/Duplicati/WebserverCore/Database/Configuration/OptionConfiguration.cs
@@ -0,0 +1,14 @@
+using Duplicati.WebserverCore.Database.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Duplicati.WebserverCore.Database.Configuration;
+
+public class OptionConfiguration : IEntityTypeConfiguration