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, Server.Database.Connection> GetDatabaseConnection; /// /// The update poll thread. /// - public static UpdatePollThread UpdatePoller; + public static UpdatePollThread UpdatePoller => Provider.GetRequiredService(); /// @@ -93,5 +99,7 @@ namespace Duplicati.Library.RestAPI /// This is the lock to be used before manipulating the shared resources /// public static readonly object MainLock = new object(); + + private static LiveControls _liveControl; } } diff --git a/Duplicati.Library.RestAPI/LiveControls.cs b/Duplicati.Library.RestAPI/LiveControls.cs index 1a51636a3..c3d0d24d0 100644 --- a/Duplicati.Library.RestAPI/LiveControls.cs +++ b/Duplicati.Library.RestAPI/LiveControls.cs @@ -18,9 +18,8 @@ // #endregion using System; -using System.Collections.Generic; -using System.Text; using Duplicati.Library.Common; +using Duplicati.Library.IO; using Duplicati.Library.RestAPI; namespace Duplicati.Server @@ -29,7 +28,7 @@ namespace Duplicati.Server /// This class keeps track of the users modifications regarding /// throttling and pause/resume /// - 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