From 5c4a0594a547c0e2112aa0dd96aa4805b212b3d9 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Sat, 8 Feb 2025 17:30:27 +0100 Subject: [PATCH 01/10] Improved TrayIcon detached. Implemented support for disconneted state with TrayIcon. Added some notification support (Windows and Linux). Changed some logic to prevent hangs on clicks. --- .../icons/linux/normal-disconnected.png | Bin 0 -> 10622 bytes .../icons/macos/dark/normal-disconnected.png | Bin 0 -> 10622 bytes .../icons/macos/light/normal-disconnected.png | Bin 0 -> 5635 bytes .../icons/windows/normal-disconnected.png | Bin 0 -> 12744 bytes .../Duplicati.GUI.TrayIcon/AvaloniaRunner.cs | 104 +++++++++++++++--- .../Duplicati.GUI.TrayIcon.csproj | 1 + .../GUI/Duplicati.GUI.TrayIcon/Program.cs | 30 +++-- .../Duplicati.GUI.TrayIcon/TrayIconBase.cs | 99 ++++++++++++----- .../Duplicati.Server.Serialization/Enums.cs | 41 +++---- Duplicati/Server/Program.cs | 2 +- 10 files changed, 205 insertions(+), 72 deletions(-) create mode 100644 Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/linux/normal-disconnected.png create mode 100644 Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/macos/dark/normal-disconnected.png create mode 100644 Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/macos/light/normal-disconnected.png create mode 100644 Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/windows/normal-disconnected.png diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/linux/normal-disconnected.png b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/linux/normal-disconnected.png new file mode 100644 index 0000000000000000000000000000000000000000..1c9813b2d1c36810e648938d53af35c89d45d90e GIT binary patch literal 10622 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

Qmas-}+)3>zG`Xpkvi5EN;qqT^Z){Y) z;QsjQ_4xFYlT>AFt3sA9`!LJD!~av{t!vS>;k%g%_Ao8nw$1E=`_hFA8&$oh1;oa# z4GyU|s`j95+nEp9{R}d;4DYY344$>?mRE4Fu>QUuO8b8t)wd{m(jn!t<7naoy$5N@ zXX5YZyoq4Q+q-6y@yCzHpIw*r8s-%g zXC`fwur7a>a(ZU$8(3mB`_(5?)-;d(owXtrq`D%`OGTNS9_V;aQWTaF)RU)t)?metG!% zxR%3-n>KBFac!-1?5+|;Tie)PRYr?fOcqtb%MQJd4PUr$VPjhx+r9e#wdc15$Hl#S z@%;JO+2SuAKiTqE$k!uKSZRj`gOdk?69=aQhaw|~q6&jz3yVU_1cnxYcs~CVg*CHO z7!-r1^z`s7SGAkW(Bcso6LC$Kkz?YjFR!k8GYKfIc=G(YwQivclYo+|q*>04%R;|Z z85Dyi1ZW)j@bK{5X)C8Pw0JnO@k%{8rjX!k#Bt!}>Q<*B53TwxOT`oZOh%;;PsW=mzQ(aempAPn6o|a?k>ZuD;me=|2sCre)%4Tm>E2ZDh*STZf(iT ztND=Y+qK$3=3X~X`(dx3u1ib3#W@$uee&dqLFy?HbFW-)F~&J%K??f%{+?cebL4g# zr=Me4clOVd3SeqE4TPA$-0K;J%;X+Uw*eS7fxaSU~e6@ zWWj=lIdf$E=2$FDS;}Br{VnCggM)opd%oYRp1ZBqtyk*k-SYcyzg3I-Gxn?BeqWVm z^uug<>#<(x+eworA8zKiJ5l@n?(?JG;qr#ZRPL^L*&5E! zH^VTQO;l9W#MD$X(bFd^OziS9-{P3(7Zy6tUA5l6^i{|*|M~B(-kQeWuzd2za(nfL z+O>}vu3K4dRP&i}U|a6(DVtQ%9v$iIo4(P^%ica!nSJ5 z^zK_p2bhn|c(z2e!SGmAc0H?3K~T=MHIX5qp{1`5E(}qV;WI9N=5xHX?A@K2L5}|Z z{^~M(msf}DgGSgkBp%k#(ke=}t&fh#=~?M>?VI8Ae@Sa)S@%5Mk^pHQhTobr!Ls<- zi@x6dIqb`%l9N2OPkdH0c-FteKt18q6wN#voxmMF`|JLS)YY$KPg5`cv0tSjEUswr zi$6a1X^1>FqdYhwaCWRcQtIv-bASR&RTL=@!*sxQw$Ohv6!~hKiq#- zVzY8+*^eOq)|#FlYq^h4nL71jZ+?yQa@QseJ-xJZb1bts>Ma<3jF07bEuH&Mno%N| zk70jdHuLwpLH2ev7V^6?G_o6xmKnF)wfLs}lFv|64xzm=bg8 z^-P8+)eF)U;_TY2>yOMdP6v%Ytyr@rWcz~Nl+;wi!bdKkp2Ux*a*w)dylN!hPd+xo z?WcM@heDHoyW?%l{%F5t3cXv8_$9jD>~lY-Tiy7_wnfcRZOvsA@NtQ zUb)q7fA80G!AJdv)+_G&RqAiFXQ}y1P+0RMFw60Bb+vrf`e2*v(>=?HX~!IvP0_zS zq7pCdtPq}&)ML4J83*6OGHGQyjZb&43A>f>t!k9y+Rt(!jPnoc45pkg;bk`K8O^1V z?Lvb+7~W1i@ba2=V|}pkGM&|pvwIBZuhdj&=<`^>x`V%hW%@itg_?z+hQrh~{j0_P zCEU?US*y!>jp^@|HJ)eH56C3u!0l3?TcB^i{$m$kqjn9)8s>Tat=lhlFmOLmlB^NdV9sOy^}1k7w^f4ehI0~| zqt7Y^`7wx}OaRZF6excPm7S-5k>L%CR^#=?a+4M6R=o^*mqR6(MB+Y_>V7nRb<=_G z0`CcrkI5H!YW)73HA?STAGuzAU)tY0yS_@r-r^=axx)cc!qWVojc4QRsP$h9Rx&U! OFnGH9xvX^X_+~x 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 literal 0 HcmV?d00001 diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/AvaloniaRunner.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/AvaloniaRunner.cs index b77cf1ba7..d8f07df43 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/AvaloniaRunner.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/AvaloniaRunner.cs @@ -19,6 +19,8 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +#nullable enable + using System; using System.Collections.Generic; using System.Linq; @@ -34,15 +36,64 @@ using Avalonia.Themes.Fluent; using Avalonia.Threading; using Duplicati.Library.AutoUpdater; using Duplicati.Library.Logging; +using DesktopNotifications.Windows; +using DesktopNotifications.FreeDesktop; namespace Duplicati.GUI.TrayIcon { + public static class AppBuilderExtensions + { + public static AppBuilder SetupDesktopNotifications(this AppBuilder builder, out DesktopNotifications.INotificationManager? manager) + { + manager = null; + try + { + DesktopNotifications.INotificationManager? realManager = null; + if (OperatingSystem.IsWindows()) + { + var context = WindowsApplicationContext.FromCurrentProcess(); + realManager = new WindowsNotificationManager(context); + } + else if (OperatingSystem.IsLinux()) + { + var context = FreeDesktopApplicationContext.FromCurrentProcess(); + realManager = new FreeDesktopNotificationManager(context); + } + else + { + // MacOS not supported + realManager = null; + } + + if (realManager == null) + return builder; + + realManager.Initialize().GetAwaiter().GetResult(); + builder.AfterSetup(b => + { + if (b.Instance?.ApplicationLifetime is IControlledApplicationLifetime lifetime) + { + lifetime.Exit += (s, e) => { realManager?.Dispose(); }; + } + }); + + manager = realManager; + } + catch (Exception ex) + { + Log.WriteErrorMessage(Log.LogTagFromType(), "DesktopNotificationInitFailed", ex, "Failed to initialize desktop notifications"); + } + return builder; + } + } + public class AvaloniaRunner : TrayIconBase { private static readonly string LOGTAG = Log.LogTagFromType(); - private AvaloniaApp application; + private AvaloniaApp? application; private ProcessBasedActionDelay actionDelayer = new ProcessBasedActionDelay(); private IEnumerable menuItems = Enumerable.Empty(); + private DesktopNotifications.INotificationManager? notificationManager; public override void Init(string[] args) { @@ -94,6 +145,7 @@ namespace Duplicati.GUI.TrayIcon .Configure(() => new AvaloniaApp() { Name = AutoUpdateSettings.AppName }) .UsePlatformDetect() .With(new MacOSPlatformOptions() { ShowInDock = false }) + .SetupDesktopNotifications(out notificationManager) .SetupWithLifetime(lifetime); #if DEBUG @@ -106,6 +158,8 @@ namespace Duplicati.GUI.TrayIcon #endif application = builder.Instance as AvaloniaApp; + if (application == null) + throw new InvalidOperationException("Failed to create Avalonia app"); application.SetMenu(menuItems); application.Configure(); @@ -114,7 +168,7 @@ namespace Duplicati.GUI.TrayIcon private async void CheckForAppInitialized(IClassicDesktopStyleApplicationLifetime lifetime) { - Exception lastEx = null; + Exception? lastEx = null; // Jump out of the UI thread, ot ensure that the check is done with posting to the UI thread, // as opposed to just calling the method directly @@ -164,8 +218,11 @@ namespace Duplicati.GUI.TrayIcon return new AvaloniaMenuItem(this, text, icon, callback, subitems); } - protected override void NotifyUser(string title, string message, NotificationType type) + public override void NotifyUser(string title, string message, NotificationType type) { + if (notificationManager == null) + return; + //var icon = Win32NativeNotifyIcon.InfoFlags.NIIF_INFO; switch (type) @@ -181,7 +238,12 @@ namespace Duplicati.GUI.TrayIcon break; } - //m_ntfIcon.ShowBalloonTip(title, message, icon); + notificationManager.ShowNotification(new DesktopNotifications.Notification() + { + Title = title, + Body = message + }, DateTimeOffset.UtcNow.AddSeconds(5)); + } protected override void Exit() @@ -209,7 +271,7 @@ namespace Duplicati.GUI.TrayIcon internal static string GetThemePath() { - var isDark = string.Equals(Application.Current.ActualThemeVariant.ToString(), "Dark", StringComparison.OrdinalIgnoreCase); + var isDark = string.Equals(Application.Current?.ActualThemeVariant.ToString(), "Dark", StringComparison.OrdinalIgnoreCase); if (OperatingSystem.IsMacOS()) return isDark ? "macos/dark" : "macos/light"; @@ -237,11 +299,12 @@ namespace Duplicati.GUI.TrayIcon public string Text { get; private set; } public Action Callback { get; private set; } - public IList SubItems { get; private set; } + public IList? SubItems { get; private set; } public bool Enabled { get; private set; } public MenuIcons Icon { get; private set; } public bool IsDefault { get; private set; } - private NativeMenuItem nativeMenuItem; + public bool Hidden { get; private set; } + private NativeMenuItem? nativeMenuItem; private readonly AvaloniaRunner parent; public AvaloniaMenuItem(AvaloniaRunner parent, string text, MenuIcons icon, Action callback, IList subitems) @@ -278,6 +341,9 @@ namespace Duplicati.GUI.TrayIcon /// private void UpdateIcon() { + if (nativeMenuItem == null) + return; + nativeMenuItem.Icon = this.Icon switch { MenuIcons.Status => AvaloniaRunner.LoadBitmap("context-menu-open.png"), @@ -301,6 +367,14 @@ namespace Duplicati.GUI.TrayIcon // Not currently supported by Avalonia, // used to set the menu bold on Windows to indicate the default entry } + + public void SetHidden(bool hidden) + { + this.Hidden = hidden; + if (nativeMenuItem != null) + parent.RunOnUIThread(() => nativeMenuItem.IsVisible = !this.Hidden); + + } #endregion public NativeMenuItem GetNativeItem() @@ -324,9 +398,9 @@ namespace Duplicati.GUI.TrayIcon public class AvaloniaApp : Application { - private Avalonia.Controls.TrayIcon trayIcon; + private Avalonia.Controls.TrayIcon? trayIcon; - private List menuItems; + private List? menuItems; public override void Initialize() { @@ -355,13 +429,17 @@ namespace Duplicati.GUI.TrayIcon default: this.trayIcon.Icon = AvaloniaRunner.LoadIcon("normal.png"); break; + case TrayIcons.Disconnected: + this.trayIcon.Icon = AvaloniaRunner.LoadIcon("normal-disconnected.png"); + break; } } public void Configure() { this.Name = Duplicati.Library.AutoUpdater.AutoUpdateSettings.AppName; - this.trayIcon.ToolTipText = this.Name; + if (this.trayIcon != null) + this.trayIcon.ToolTipText = this.Name; } public void SetMenu(IEnumerable menuItems) @@ -413,10 +491,10 @@ namespace Duplicati.GUI.TrayIcon public bool IsEnabled(LogEventLevel level, string area) => level >= _minLevel; - public void Log(LogEventLevel level, string area, object source, string messageTemplate) - => Log(level, area, source, messageTemplate, null); + public void Log(LogEventLevel level, string area, object? source, string messageTemplate) + => Log(level, area, source, messageTemplate, []); - public void Log(LogEventLevel level, string area, object source, string messageTemplate, params object[] propertyValues) + public void Log(LogEventLevel level, string area, object? source, string messageTemplate, params object?[] propertyValues) => Console.WriteLine($"Avalonia [{level}]: {source} {messageTemplate} {string.Join(" ", propertyValues)}"); } diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj index 2e38301db..410d7ae12 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj @@ -32,6 +32,7 @@ + diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs index 28cff9188..d9e924630 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs @@ -24,6 +24,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; +using System.Net.Http; +using System.Threading; using Duplicati.Library.AutoUpdater; using Duplicati.Library.Interface; using Duplicati.Library.RestAPI; @@ -85,7 +87,7 @@ namespace Duplicati.GUI.TrayIcon if (OperatingSystem.IsWindows() && !Library.Utility.Utility.ParseBoolOption(options, DETACHED_PROCESS)) Library.Utility.Win32.AttachConsole(Library.Utility.Win32.ATTACH_PARENT_PROCESS); - + if (HelpOptionExtensions.IsArgumentAnyHelpString(args)) { Console.WriteLine("Supported commandline arguments:"); @@ -204,11 +206,19 @@ No password provided, unable to connect to server, exiting"); do { + if (reSpawn > 0) + Thread.Sleep(1000); + try { ServicePointManager.SecurityProtocol = SecurityProtocolType.SystemDefault; using (Connection = new HttpServerConnection(serverURL, password, passwordSource, disableTrayIconLogin, acceptedHostCertificate, options)) { + // Make sure we have the latest status + // This will throw an exception if the server is not running, + // or the password is incorrect + Connection.UpdateStatus().Await(); + using (var tk = RunTrayIcon()) { if (hosted != null && Server.Program.ApplicationInstance != null) @@ -219,16 +229,20 @@ No password provided, unable to connect to server, exiting"); // TODO: If we change to hosted browser this should be a callback if (openui) { - try + Connection.GetStatusWindowURLAsync().ContinueWith(t => { - tk.ShowUrlInWindow(Connection.StatusWindowURL); + if (t.IsFaulted) + { + Console.WriteLine("Failed to get status window URL: " + t.Exception.Message); + tk.NotifyUser("Failed to get status window URL", t.Exception.Message, NotificationType.Error); + return; + } + tk.ShowUrlInWindow(t.Result); Server.Program.IsFirstRun = false; Server.Program.ServerPortChanged = false; - } - catch - { - } + + }); } // If the server shuts down, shut down the tray-icon as well @@ -255,7 +269,7 @@ No password provided, unable to connect to server, exiting"); } } } - catch (WebException ex) + catch (HttpRequestException ex) { System.Diagnostics.Trace.WriteLine("Request error: " + ex); Console.WriteLine("Request error: " + ex); diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayIconBase.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayIconBase.cs index 4906daf85..1b769718f 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayIconBase.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayIconBase.cs @@ -1,22 +1,22 @@ -// Copyright (C) 2025, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// Copyright (C) 2025, The Duplicati Team +// https://duplicati.com, hello@duplicati.com +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; @@ -43,7 +43,8 @@ namespace Duplicati.GUI.TrayIcon IdleWarning, IdleError, PausedError, - RunningError + RunningError, + Disconnected } public enum WindowIcons @@ -64,11 +65,16 @@ namespace Duplicati.GUI.TrayIcon void SetDefault(bool isDefault); void SetIcon(MenuIcons icon); void SetText(string text); + void SetEnabled(bool enabled); + void SetHidden(bool hidden); } public abstract class TrayIconBase : IDisposable { + protected IMenuItem m_reconnectMenu; + protected IMenuItem m_openMenu; protected IMenuItem m_pauseMenu; + protected IMenuItem m_quitMenu; protected bool m_stateIsPaused; protected Action m_onSingleClick; protected Action m_onDoubleClick; @@ -105,7 +111,7 @@ namespace Duplicati.GUI.TrayIcon protected abstract void SetMenu(IEnumerable items); - protected abstract void NotifyUser(string title, string message, NotificationType type); + public abstract void NotifyUser(string title, string message, NotificationType type); protected virtual void RegisterStatusUpdateCallback() { @@ -149,23 +155,49 @@ namespace Duplicati.GUI.TrayIcon protected IEnumerable BuildMenu() { - var tmp = CreateMenuItem("Open", MenuIcons.Status, OnStatusClicked, null); - tmp.SetDefault(true); + m_reconnectMenu = CreateMenuItem("Reconnect", MenuIcons.Resume, Reconnect, null); + m_reconnectMenu.SetHidden(true); + m_openMenu = CreateMenuItem("Open", MenuIcons.Status, OnStatusClicked, null); + m_openMenu.SetDefault(true); return new IMenuItem[] { - tmp, + m_reconnectMenu, + m_openMenu, m_pauseMenu = CreateMenuItem("Pause", MenuIcons.Pause, OnPauseClicked, null ), - CreateMenuItem("Quit", MenuIcons.Quit, OnQuitClicked, null), + m_quitMenu = CreateMenuItem("Quit", MenuIcons.Quit, OnQuitClicked, null), }; } + private void Reconnect() + { + Program.Connection.UpdateStatus().ContinueWith(t => + { + if (t.IsFaulted) + { + Console.WriteLine("Failed to reconnect: " + t.Exception.Message); + NotifyUser("Failed to reconnect", t.Exception.Message, NotificationType.Error); + } + + }); + } + protected void ShowStatusWindow() { - var window = ShowUrlInWindow(Program.Connection.StatusWindowURL); - if (window != null) + Program.Connection.GetStatusWindowURLAsync().ContinueWith(t => { - window.SetIcon(WindowIcons.Regular); - window.SetTitle("Duplicati status"); - } + if (t.IsFaulted) + { + Console.WriteLine("Failed to get status window URL: " + t.Exception.Message); + NotifyUser("Failed to get status window URL", t.Exception.Message, NotificationType.Error); + return; + } + + var window = ShowUrlInWindow(t.Result); + if (window != null) + { + window.SetIcon(WindowIcons.Regular); + window.SetTitle("Duplicati status"); + } + }); } protected void OnStatusClicked() @@ -207,6 +239,9 @@ namespace Duplicati.GUI.TrayIcon case SuggestedStatusIcon.Paused: this.SetIcon(TrayIcons.Paused); break; + case SuggestedStatusIcon.Disconnected: + this.SetIcon(TrayIcons.Disconnected); + break; case SuggestedStatusIcon.Ready: default: this.SetIcon(TrayIcons.Idle); @@ -226,6 +261,10 @@ namespace Duplicati.GUI.TrayIcon m_pauseMenu.SetText("Resume"); m_stateIsPaused = true; } + + m_openMenu.SetHidden(status.SuggestedStatusIcon == SuggestedStatusIcon.Disconnected); + m_pauseMenu.SetHidden(status.SuggestedStatusIcon == SuggestedStatusIcon.Disconnected); + m_reconnectMenu.SetHidden(status.SuggestedStatusIcon != SuggestedStatusIcon.Disconnected); }); } diff --git a/Duplicati/Server/Duplicati.Server.Serialization/Enums.cs b/Duplicati/Server/Duplicati.Server.Serialization/Enums.cs index 31c615634..714e450d3 100755 --- a/Duplicati/Server/Duplicati.Server.Serialization/Enums.cs +++ b/Duplicati/Server/Duplicati.Server.Serialization/Enums.cs @@ -1,22 +1,22 @@ -// Copyright (C) 2025, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// Copyright (C) 2025, The Duplicati Team +// https://duplicati.com, hello@duplicati.com +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; @@ -53,7 +53,8 @@ namespace Duplicati.Server.Serialization ReadyError, Paused, Active, - ActivePaused + ActivePaused, + Disconnected } diff --git a/Duplicati/Server/Program.cs b/Duplicati/Server/Program.cs index 25b37a954..2886f1bba 100644 --- a/Duplicati/Server/Program.cs +++ b/Duplicati/Server/Program.cs @@ -265,7 +265,7 @@ namespace Duplicati.Server DataConnection.LogError(null, "Error in updater", obj); }; - DuplicatiWebserver = StartWebServer(commandlineOptions, DataConnection).ConfigureAwait(false).GetAwaiter().GetResult(); + DuplicatiWebserver = StartWebServer(commandlineOptions, DataConnection).Await(); UpdatePoller.Init(Library.Utility.Utility.ParseBoolOption(commandlineOptions, DISABLE_UPDATE_CHECK_OPTION)); From 4be739ff2ff35750693ec47427e66571a97bdca5 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Sat, 8 Feb 2025 17:31:49 +0100 Subject: [PATCH 02/10] Updated logic for reconnect. This fixes a case where using a wrong password could cause an infinite loop. --- .../HttpServerConnection.cs | 122 ++++++++++-------- 1 file changed, 65 insertions(+), 57 deletions(-) diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs index cf610569a..d6f6b7a54 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs @@ -1,22 +1,22 @@ -// Copyright (C) 2025, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// Copyright (C) 2025, The Duplicati Team +// https://duplicati.com, hello@duplicati.com +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; @@ -88,6 +88,7 @@ namespace Duplicati.GUI.TrayIcon private readonly string m_baseUri; private string m_password; private string m_accesstoken; + private bool m_isTryingWithPassword; public delegate void StatusUpdateDelegate(IServerStatus status); public event StatusUpdateDelegate OnStatusUpdated; @@ -161,10 +162,6 @@ namespace Duplicati.GUI.TrayIcon } }; - // TODO: Not nice to do in constructor - // Get a connection - UpdateStatus(false); - m_waitLock = new AutoResetEvent(false); m_requestThread = new Thread(ThreadRunner); m_pollThread = new Thread(LongPollRunner); @@ -176,11 +173,14 @@ namespace Duplicati.GUI.TrayIcon m_pollThread.Start(); } - private void UpdateStatus(bool longpoll) + public Task UpdateStatus() + => UpdateStatusAsync(false); + + private async Task UpdateStatusAsync(bool longpoll) { var query = longpoll ? $"?longpoll=true&lastEventId={m_lastEventId}&duration={LONGPOLL_TIMEOUT}" : ""; - m_status = PerformRequest("GET", $"/serverstate{query}", null, longpoll ? Library.Utility.Timeparser.ParseTimeSpan(LONGPOLL_TIMEOUT) : null); + m_status = await PerformRequestAsync("GET", $"/serverstate{query}", null, longpoll ? Library.Utility.Timeparser.ParseTimeSpan(LONGPOLL_TIMEOUT) : null); m_lastEventId = m_status.LastEventID; if (OnStatusUpdated != null) @@ -189,19 +189,19 @@ namespace Duplicati.GUI.TrayIcon if (m_lastNotificationId != m_status.LastNotificationUpdateID) { m_lastNotificationId = m_status.LastNotificationUpdateID; - UpdateNotifications(); + await UpdateNotificationsAsync().ConfigureAwait(false); } if (m_lastDataUpdateId != m_status.LastDataUpdateID) { m_lastDataUpdateId = m_status.LastDataUpdateID; - UpdateApplicationSettings(); + await UpdateApplicationSettingsAsync().ConfigureAwait(false); } } - private void UpdateNotifications() + private async Task UpdateNotificationsAsync() { - var notifications = PerformRequest("GET", "/notifications", null, null); + var notifications = await PerformRequestAsync("GET", "/notifications", null, null).ConfigureAwait(false); if (notifications != null) { foreach (var n in notifications.Where(x => x.Timestamp > m_firstNotificationTime)) @@ -213,9 +213,9 @@ namespace Duplicati.GUI.TrayIcon } } - private void UpdateApplicationSettings() + private async Task UpdateApplicationSettingsAsync() { - var settings = PerformRequest>("GET", "/serversettings", null, null); + var settings = await PerformRequestAsync>("GET", "/serversettings", null, null).ConfigureAwait(false); if (settings != null && settings.TryGetValue("disable-tray-icon-login", out var str)) m_disableTrayIconLogin = Library.Utility.Utility.ParseBool(str, false); } @@ -235,12 +235,15 @@ namespace Duplicati.GUI.TrayIcon if (waitTime.TotalSeconds > 0) Thread.Sleep(waitTime); started = DateTime.Now; - UpdateStatus(true); + UpdateStatusAsync(true).Await(); errorCount = 0; } catch (Exception ex) { errorCount++; + // More than 1 error, and we are disconnected + if (errorCount >= 2) + Task.Run(() => { OnStatusUpdated?.Invoke(new ServerStatusImpl(LiveControlState.Paused, SuggestedStatusIcon.Disconnected, m_lastEventId, m_lastDataUpdateId, m_lastNotificationId)); }).Await(); System.Diagnostics.Trace.WriteLine("Request error: " + ex.Message); Library.Logging.Log.WriteWarningMessage(LOGTAG, "TrayIconRequestError", ex, "Failed to get response"); } @@ -269,7 +272,7 @@ namespace Duplicati.GUI.TrayIcon if (req != null) { any = true; - PerformRequest(req.Method, req.Endpoint, req.Body, req.Timeout); + PerformRequestAsync(req.Method, req.Endpoint, req.Body, req.Timeout).Await(); } } while (req != null); @@ -300,18 +303,18 @@ namespace Duplicati.GUI.TrayIcon private sealed record SigninResponse(string AccessToken); - private T PerformRequest(string method, string urlfragment, string body, TimeSpan? timeout) + private async Task PerformRequestAsync(string method, string urlfragment, string body, TimeSpan? timeout) { if (string.IsNullOrWhiteSpace(m_accesstoken) && !urlfragment.StartsWith("/auth/")) - ObtainAccessToken(); + await ObtainAccessTokenAsync().ConfigureAwait(false); - var hasTriedPassword = false; + var hasTriedPassword = m_isTryingWithPassword; while (true) { try { - return PerformRequestInternal(method, urlfragment, body, timeout); + return await PerformRequestInternalAsync(method, urlfragment, body, timeout).ConfigureAwait(false); } catch (Exception ex) when (ex is HttpRequestException hex && (hex.StatusCode == HttpStatusCode.Unauthorized || hex.StatusCode == HttpStatusCode.Forbidden) @@ -321,20 +324,31 @@ namespace Duplicati.GUI.TrayIcon if (hasTriedPassword) throw; + // TODO: This error handling is error prone and can end up in infinite recursion + // Should rewrite the entire class and use websockets instead + // Only try once, and clear the token for the next try hasTriedPassword = true; m_accesstoken = null; - ObtainAccessToken(); + try + { + m_isTryingWithPassword = true; + await ObtainAccessTokenAsync().ConfigureAwait(false); + } + finally + { + m_isTryingWithPassword = false; + } } } } - private void ObtainAccessToken() + private async Task ObtainAccessTokenAsync() { - var token = ObtainSignInToken(); + var token = await ObtainSignInTokenAsync().ConfigureAwait(false); if (string.IsNullOrWhiteSpace(token)) return; - m_accesstoken = PerformRequestInternal("POST", "/auth/signin", JsonSerializer.Serialize(new { SigninToken = token }), null).AccessToken; + m_accesstoken = (await PerformRequestInternalAsync("POST", "/auth/signin", JsonSerializer.Serialize(new { SigninToken = token }), null).ConfigureAwait(false)).AccessToken; } private async Task PerformRequestInternalAsync(string method, string endpoint, string body, TimeSpan? timeout) @@ -358,13 +372,10 @@ namespace Duplicati.GUI.TrayIcon if (typeof(T) == typeof(string)) return (T)(object)await response.Content.ReadAsStringAsync().ConfigureAwait(false); - using (var stream = await response.Content.ReadAsStreamAsync()) + using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) return await JsonSerializer.DeserializeAsync(stream, serializerOptions).ConfigureAwait(false); } - private T PerformRequestInternal(string method, string endpoint, string body, TimeSpan? timeout) - => PerformRequestInternalAsync(method, endpoint, body, timeout).Await(); - private void ExecuteAndNotify(string method, string urifragment, string body) { lock (m_lock) @@ -391,10 +402,10 @@ namespace Duplicati.GUI.TrayIcon } private sealed record SigninTokenResponse(string Token); - private string IssueSigninToken(string password) - => PerformRequest("POST", "/auth/issuesignintoken", JsonSerializer.Serialize(new { Password = password }), null).Token; + private async Task IssueSigninTokenAsync(string password) + => (await PerformRequestInternalAsync("POST", "/auth/issuesignintoken", JsonSerializer.Serialize(new { Password = password }), null).ConfigureAwait(false)).Token; - private string ObtainSignInToken() + private async Task ObtainSignInTokenAsync() { string signinjwt = null; @@ -413,20 +424,17 @@ namespace Duplicati.GUI.TrayIcon // If we know the password, issue a token from the API if (string.IsNullOrWhiteSpace(signinjwt) && !string.IsNullOrWhiteSpace(m_password)) - signinjwt = IssueSigninToken(m_password); + signinjwt = await IssueSigninTokenAsync(m_password); return signinjwt; } - public string StatusWindowURL + public async Task GetStatusWindowURLAsync() { - get - { - var signinjwt = m_disableTrayIconLogin ? null : ObtainSignInToken(); - return string.IsNullOrWhiteSpace(signinjwt) - ? m_baseUri + STATUS_WINDOW - : m_baseUri + SIGNIN_WINDOW + $"?token={signinjwt}"; - } + var signinjwt = m_disableTrayIconLogin ? null : await ObtainSignInTokenAsync().ConfigureAwait(false); + return string.IsNullOrWhiteSpace(signinjwt) + ? m_baseUri + STATUS_WINDOW + : m_baseUri + SIGNIN_WINDOW + $"?token={signinjwt}"; } } } From d9258bfad2104a002c0d2b646fceef6cc3181d78 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Sun, 9 Feb 2025 09:48:04 +0100 Subject: [PATCH 03/10] Removed desktop notifications, it did not work --- .../Duplicati.GUI.TrayIcon/AvaloniaRunner.cs | 60 ------------------- .../Duplicati.GUI.TrayIcon.csproj | 1 - 2 files changed, 61 deletions(-) diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/AvaloniaRunner.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/AvaloniaRunner.cs index d8f07df43..ae31a1828 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/AvaloniaRunner.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/AvaloniaRunner.cs @@ -36,64 +36,15 @@ using Avalonia.Themes.Fluent; using Avalonia.Threading; using Duplicati.Library.AutoUpdater; using Duplicati.Library.Logging; -using DesktopNotifications.Windows; -using DesktopNotifications.FreeDesktop; namespace Duplicati.GUI.TrayIcon { - public static class AppBuilderExtensions - { - public static AppBuilder SetupDesktopNotifications(this AppBuilder builder, out DesktopNotifications.INotificationManager? manager) - { - manager = null; - try - { - DesktopNotifications.INotificationManager? realManager = null; - if (OperatingSystem.IsWindows()) - { - var context = WindowsApplicationContext.FromCurrentProcess(); - realManager = new WindowsNotificationManager(context); - } - else if (OperatingSystem.IsLinux()) - { - var context = FreeDesktopApplicationContext.FromCurrentProcess(); - realManager = new FreeDesktopNotificationManager(context); - } - else - { - // MacOS not supported - realManager = null; - } - - if (realManager == null) - return builder; - - realManager.Initialize().GetAwaiter().GetResult(); - builder.AfterSetup(b => - { - if (b.Instance?.ApplicationLifetime is IControlledApplicationLifetime lifetime) - { - lifetime.Exit += (s, e) => { realManager?.Dispose(); }; - } - }); - - manager = realManager; - } - catch (Exception ex) - { - Log.WriteErrorMessage(Log.LogTagFromType(), "DesktopNotificationInitFailed", ex, "Failed to initialize desktop notifications"); - } - return builder; - } - } - public class AvaloniaRunner : TrayIconBase { private static readonly string LOGTAG = Log.LogTagFromType(); private AvaloniaApp? application; private ProcessBasedActionDelay actionDelayer = new ProcessBasedActionDelay(); private IEnumerable menuItems = Enumerable.Empty(); - private DesktopNotifications.INotificationManager? notificationManager; public override void Init(string[] args) { @@ -145,7 +96,6 @@ namespace Duplicati.GUI.TrayIcon .Configure(() => new AvaloniaApp() { Name = AutoUpdateSettings.AppName }) .UsePlatformDetect() .With(new MacOSPlatformOptions() { ShowInDock = false }) - .SetupDesktopNotifications(out notificationManager) .SetupWithLifetime(lifetime); #if DEBUG @@ -220,9 +170,6 @@ namespace Duplicati.GUI.TrayIcon public override void NotifyUser(string title, string message, NotificationType type) { - if (notificationManager == null) - return; - //var icon = Win32NativeNotifyIcon.InfoFlags.NIIF_INFO; switch (type) @@ -237,13 +184,6 @@ namespace Duplicati.GUI.TrayIcon //icon = Win32NativeNotifyIcon.InfoFlags.NIIF_ERROR; break; } - - notificationManager.ShowNotification(new DesktopNotifications.Notification() - { - Title = title, - Body = message - }, DateTimeOffset.UtcNow.AddSeconds(5)); - } protected override void Exit() diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj index 410d7ae12..2e38301db 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj @@ -32,7 +32,6 @@ - From 273280c9b757554fee80e8d0af46fa989604b1ed Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Sun, 9 Feb 2025 10:28:44 +0100 Subject: [PATCH 04/10] Added handler for ctrl+c --- Duplicati/Server/Program.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Duplicati/Server/Program.cs b/Duplicati/Server/Program.cs index 2886f1bba..9bedbfcf8 100644 --- a/Duplicati/Server/Program.cs +++ b/Duplicati/Server/Program.cs @@ -311,6 +311,21 @@ namespace Duplicati.Server ApplicationExitEvent.Set(); }); + var stopCounter = 0; + Console.CancelKeyPress += (sender, e) => + { + if (Interlocked.Increment(ref stopCounter) <= 1) + { + Log.WriteInformationMessage(LOGTAG, "CancelKeyPressed", "Cancel key pressed, stopping server"); + Task.Run(() => DuplicatiWebserver?.Stop()); + } + else + { + Log.WriteWarningMessage(LOGTAG, "CancelKeyPressed", null, "Cancel key pressed twice, terminating now"); + Environment.Exit(0); + } + }; + ServerStartedEvent.Set(); ApplicationExitEvent.WaitOne(); } From da1f6562ea7dc9eae9857db525fb1133953c5059 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Sun, 9 Feb 2025 10:28:56 +0100 Subject: [PATCH 05/10] Added option to log to console --- Duplicati/Library/RestAPI/Strings.cs | 1 + Duplicati/Server/Program.cs | 51 +++++++++++++++++++--------- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/Duplicati/Library/RestAPI/Strings.cs b/Duplicati/Library/RestAPI/Strings.cs index a7e7d3e0c..e7321da48 100644 --- a/Duplicati/Library/RestAPI/Strings.cs +++ b/Duplicati/Library/RestAPI/Strings.cs @@ -41,6 +41,7 @@ Error message: {0}", message); } public static string SkippingSourceArgumentsOnNonBackupOperation { get { return @"The --source argument was specified in the parameter file, but the current operation is not a backup operation, so the argument is ignored"; } } public static string LogfileCommandDescription { get { return LC.L(@"Output log information to the file given"); } } public static string LoglevelCommandDescription { get { return LC.L(@"Determine the amount of information written in the log file"); } } + public static string LogConsoleDescription { get { return LC.L(@"Output log information to the console"); } } public static string PortablemodeCommandDescription { get { return LC.L(@"Activate portable mode where the database is placed below the program executable"); } } public static string SeriousError(string message) { return LC.L(@"A serious error occurred in Duplicati: {0}", message); } public static string TearDownError(string message) { return LC.L(@"An error occurred on server tear down: {0}", message); } diff --git a/Duplicati/Server/Program.cs b/Duplicati/Server/Program.cs index 9bedbfcf8..ed00a0456 100644 --- a/Duplicati/Server/Program.cs +++ b/Duplicati/Server/Program.cs @@ -21,7 +21,6 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -52,6 +51,10 @@ namespace Duplicati.Server private const string REQUIRE_DB_ENCRYPTION_KEY_OPTION = "require-db-encryption-key"; private const string SETTINGS_ENCRYPTION_KEY_OPTION = "settings-encryption-key"; private const string DISABLE_UPDATE_CHECK_OPTION = "disable-update-check"; + private const string LOG_FILE_OPTION = "log-file"; + private const string LOG_LEVEL_OPTION = "log-level"; + private const string LOG_CONSOLE_OPTION = "log-console"; + #if DEBUG private const bool DEBUG_MODE = true; @@ -683,30 +686,45 @@ namespace Duplicati.Server private static async Task ApplySecretProvider(Dictionary commandlineOptions, CancellationToken cancellationToken) => FIXMEGlobal.SecretProvider = await SecretProviderHelper.ApplySecretProviderAsync([], [], commandlineOptions, TempFolder.SystemTempPath, FIXMEGlobal.SecretProvider, cancellationToken).ConfigureAwait(false); + private class ConsoleLogDestination(LogMessageType level) : ILogDestination + { + public void WriteMessage(LogEntry entry) + { + if (entry.Level >= level) + Console.WriteLine(entry.AsString(true)); + } + } + private static void ConfigureLogging(Dictionary commandlineOptions) { //Log various information in the logfile - if (DEBUG_MODE && !commandlineOptions.ContainsKey("log-file")) + if (DEBUG_MODE && !commandlineOptions.ContainsKey(LOG_FILE_OPTION)) { var prefix = System.Reflection.Assembly.GetEntryAssembly().GetName().Name.StartsWith("Duplicati.Server") ? "server" : "trayicon"; - commandlineOptions["log-file"] = System.IO.Path.Combine(StartupPath, $"Duplicati-{prefix}.debug.log"); - commandlineOptions["log-level"] = Duplicati.Library.Logging.LogMessageType.Profiling.ToString(); - if (System.IO.File.Exists(commandlineOptions["log-file"])) - { - System.IO.File.Delete(commandlineOptions["log-file"]); - } + commandlineOptions[LOG_FILE_OPTION] = System.IO.Path.Combine(StartupPath, $"Duplicati-{prefix}.debug.log"); + commandlineOptions[LOG_LEVEL_OPTION] = Duplicati.Library.Logging.LogMessageType.Profiling.ToString(); + if (System.IO.File.Exists(commandlineOptions[LOG_FILE_OPTION])) + System.IO.File.Delete(commandlineOptions[LOG_FILE_OPTION]); } - // Setup the log redirect - Library.Logging.Log.StartScope(LogHandler, null); + Log.StartScope(LogHandler, null); - if (commandlineOptions.ContainsKey("log-file")) + if (commandlineOptions.ContainsKey(LOG_FILE_OPTION)) { var loglevel = Library.Logging.LogMessageType.Warning; - if (commandlineOptions.ContainsKey("log-level")) - Enum.TryParse(commandlineOptions["log-level"], true, out loglevel); + if (commandlineOptions.ContainsKey(LOG_LEVEL_OPTION)) + Enum.TryParse(commandlineOptions[LOG_LEVEL_OPTION], true, out loglevel); - LogHandler.SetServerFile(commandlineOptions["log-file"], loglevel); + LogHandler.SetServerFile(commandlineOptions[LOG_FILE_OPTION], loglevel); + } + + if (Library.Utility.Utility.ParseBoolOption(commandlineOptions, LOG_CONSOLE_OPTION)) + { + var loglevel = Library.Logging.LogMessageType.Information; + if (commandlineOptions.ContainsKey(LOG_LEVEL_OPTION)) + Enum.TryParse(commandlineOptions[LOG_LEVEL_OPTION], true, out loglevel); + + LogHandler.AppendLogDestination(new ConsoleLogDestination(loglevel), loglevel); } if (commandlineOptions.TryGetValue(WINDOWS_EVENTLOG_OPTION, out var source) && !string.IsNullOrEmpty(source)) @@ -957,8 +975,9 @@ namespace Duplicati.Server new Duplicati.Library.Interface.CommandLineArgument("help", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.HelpCommandDescription, Strings.Program.HelpCommandDescription), new Duplicati.Library.Interface.CommandLineArgument("parameters-file", Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.ParametersFileOptionShort, Strings.Program.ParametersFileOptionLong2, "", ParameterFileOptionStrings), new Duplicati.Library.Interface.CommandLineArgument(DataFolderManager.PORTABLE_MODE_OPTION, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.PortablemodeCommandDescription, Strings.Program.PortablemodeCommandDescription, DataFolderManager.PORTABLE_MODE.ToString().ToLowerInvariant()), - new Duplicati.Library.Interface.CommandLineArgument("log-file", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.LogfileCommandDescription, Strings.Program.LogfileCommandDescription), - new Duplicati.Library.Interface.CommandLineArgument("log-level", Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Enumeration, Strings.Program.LoglevelCommandDescription, Strings.Program.LoglevelCommandDescription, Library.Logging.LogMessageType.Warning.ToString(), null, Enum.GetNames(typeof(Duplicati.Library.Logging.LogMessageType))), + new Duplicati.Library.Interface.CommandLineArgument(LOG_FILE_OPTION, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.LogfileCommandDescription, Strings.Program.LogfileCommandDescription), + new Duplicati.Library.Interface.CommandLineArgument(LOG_LEVEL_OPTION, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Enumeration, Strings.Program.LoglevelCommandDescription, Strings.Program.LoglevelCommandDescription, Library.Logging.LogMessageType.Warning.ToString(), null, Enum.GetNames(typeof(Duplicati.Library.Logging.LogMessageType))), + new Duplicati.Library.Interface.CommandLineArgument(LOG_CONSOLE_OPTION, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean, Strings.Program.LogConsoleDescription, Strings.Program.LogConsoleDescription, false.ToString()), new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBROOT, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path, Strings.Program.WebserverWebrootDescription, Strings.Program.WebserverWebrootDescription, WebServerLoader.DEFAULT_OPTION_WEBROOT), new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_PORT, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverPortDescription, Strings.Program.WebserverPortDescription, WebServerLoader.DEFAULT_OPTION_PORT.ToString()), new Duplicati.Library.Interface.CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_DISABLEHTTPS, Duplicati.Library.Interface.CommandLineArgument.ArgumentType.String, Strings.Program.WebserverDisableHTTPSDescription, Strings.Program.WebserverDisableHTTPSDescription), From f22026cf5985a999e76d7375c79340abe0cb92c3 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Sun, 9 Feb 2025 10:52:47 +0100 Subject: [PATCH 06/10] Fixed a compile issue with older SDKs --- Duplicati/Library/Utility/Utility.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Duplicati/Library/Utility/Utility.cs b/Duplicati/Library/Utility/Utility.cs index 4d2d8e423..fdb2a6713 100644 --- a/Duplicati/Library/Utility/Utility.cs +++ b/Duplicati/Library/Utility/Utility.cs @@ -807,7 +807,7 @@ namespace Duplicati.Library.Utility return @default; var flags = 0; - foreach (var s in value.Split([','], StringSplitOptions.RemoveEmptyEntries)) + foreach (var s in value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { var trimmed = s.Trim(); if (Enum.TryParse(trimmed, true, out T flag)) From 3da02c552c932834fe90603c324233685b596c8e Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Sun, 9 Feb 2025 11:01:21 +0100 Subject: [PATCH 07/10] Added force terminate support for Windows --- Duplicati/Server/Program.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Duplicati/Server/Program.cs b/Duplicati/Server/Program.cs index ed00a0456..33aedb0bf 100644 --- a/Duplicati/Server/Program.cs +++ b/Duplicati/Server/Program.cs @@ -325,7 +325,10 @@ namespace Duplicati.Server else { Log.WriteWarningMessage(LOGTAG, "CancelKeyPressed", null, "Cancel key pressed twice, terminating now"); - Environment.Exit(0); + if (OperatingSystem.IsWindows()) + Environment.FailFast("Cancel key pressed twice, terminating now"); + else + Environment.Exit(0); } }; From bb3ea2f6cdf101974b86f0b566624cda52602a00 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Sun, 9 Feb 2025 11:05:54 +0100 Subject: [PATCH 08/10] Fixed a rare startup issue --- Duplicati/Library/RestAPI/Scheduler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Duplicati/Library/RestAPI/Scheduler.cs b/Duplicati/Library/RestAPI/Scheduler.cs index ba7bd40cf..281efc0fd 100644 --- a/Duplicati/Library/RestAPI/Scheduler.cs +++ b/Duplicati/Library/RestAPI/Scheduler.cs @@ -152,7 +152,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() ?? []; } } ///

From 42044543ff84f3165257f2c3996cec6101d8c64b Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Sun, 9 Feb 2025 11:28:52 +0100 Subject: [PATCH 09/10] Added support for disabled signin tokens and trayicon --- .../HttpServerConnection.cs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs index d6f6b7a54..6a489ac09 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs @@ -345,9 +345,16 @@ namespace Duplicati.GUI.TrayIcon private async Task ObtainAccessTokenAsync() { + if (!string.IsNullOrWhiteSpace(m_password)) + { + m_accesstoken = (await PerformRequestInternalAsync("POST", "/auth/login", JsonSerializer.Serialize(new { Password = m_password }), null).ConfigureAwait(false)).AccessToken; + return; + } + var token = await ObtainSignInTokenAsync().ConfigureAwait(false); if (string.IsNullOrWhiteSpace(token)) return; + m_accesstoken = (await PerformRequestInternalAsync("POST", "/auth/signin", JsonSerializer.Serialize(new { SigninToken = token }), null).ConfigureAwait(false)).AccessToken; } @@ -411,12 +418,20 @@ namespace Duplicati.GUI.TrayIcon // If we host the server, issue the token from the service if (FIXMEGlobal.IsServerStarted && m_passwordSource == Program.PasswordSource.HostedServer) + { + if (FIXMEGlobal.DataConnection.ApplicationSettings.DisableSigninTokens) + return null; + signinjwt = FIXMEGlobal.Provider.GetRequiredService().CreateSigninToken("trayicon"); + } // If we have database access, grab the issuer key from the db and issue a token if (string.IsNullOrWhiteSpace(signinjwt) && m_passwordSource == Program.PasswordSource.Database) { Program.databaseConnection.ApplicationSettings.ReloadSettings(); + if (Program.databaseConnection.ApplicationSettings.DisableSigninTokens) + return null; + var cfg = Program.databaseConnection.ApplicationSettings.JWTConfig; if (!string.IsNullOrWhiteSpace(cfg)) signinjwt = new JWTTokenProvider(JsonSerializer.Deserialize(cfg)).CreateSigninToken("trayicon"); @@ -431,7 +446,18 @@ namespace Duplicati.GUI.TrayIcon public async Task GetStatusWindowURLAsync() { - var signinjwt = m_disableTrayIconLogin ? null : await ObtainSignInTokenAsync().ConfigureAwait(false); + string signinjwt = null; + if (!m_disableTrayIconLogin) + { + try + { + signinjwt = await ObtainSignInTokenAsync().ConfigureAwait(false); + } + catch + { + } + } + return string.IsNullOrWhiteSpace(signinjwt) ? m_baseUri + STATUS_WINDOW : m_baseUri + SIGNIN_WINDOW + $"?token={signinjwt}"; From 68d11a2a8bdb1832550dbc0aa98928e844d7af80 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Mon, 10 Feb 2025 10:22:16 +0100 Subject: [PATCH 10/10] Improved support for disabled signin tokens and trayicon with a hosted server --- .../Duplicati.GUI.TrayIcon/HttpServerConnection.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs index 6a489ac09..481744afc 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs @@ -345,16 +345,27 @@ namespace Duplicati.GUI.TrayIcon private async Task ObtainAccessTokenAsync() { + // If we host the server, issue the access token from the service + if (FIXMEGlobal.IsServerStarted && m_passwordSource == Program.PasswordSource.HostedServer) + { + var provider = FIXMEGlobal.Provider.GetRequiredService(); + m_accesstoken = provider.CreateAccessToken("trayicon", provider.TemporaryFamilyId); + return; + } + + // If we know the password, issue a token from the API if (!string.IsNullOrWhiteSpace(m_password)) { m_accesstoken = (await PerformRequestInternalAsync("POST", "/auth/login", JsonSerializer.Serialize(new { Password = m_password }), null).ConfigureAwait(false)).AccessToken; return; } + // Otherwise, try to get a signin token var token = await ObtainSignInTokenAsync().ConfigureAwait(false); if (string.IsNullOrWhiteSpace(token)) return; + // Use the signin token to get an access token m_accesstoken = (await PerformRequestInternalAsync("POST", "/auth/signin", JsonSerializer.Serialize(new { SigninToken = token }), null).ConfigureAwait(false)).AccessToken; }