From 3719935b1ddf66350ab65a5a9f3921287e27da58 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Tue, 24 Jun 2025 16:13:59 +0200 Subject: [PATCH 1/7] No exempt throttle backends This PR removes the default value of `file` as a throttle exempt backend, so all backends are subject to throttle by default. This change makes the option easier to work with as the empty string could not be used to undo the default value, and fixing this would make the options be applied inconsistently. Also fixed two errors in the help text for the option. --- Duplicati/Library/Main/Options.cs | 4 ++-- Duplicati/Library/Main/Strings.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Duplicati/Library/Main/Options.cs b/Duplicati/Library/Main/Options.cs index 5ba524aa3..191cf88c7 100644 --- a/Duplicati/Library/Main/Options.cs +++ b/Duplicati/Library/Main/Options.cs @@ -97,7 +97,7 @@ namespace Duplicati.Library.Main /// /// The backends where throttling is disabled by default /// - private const string DEFAULT_THROTTLE_DISABLED_BACKENDS = "file"; + private const string DEFAULT_THROTTLE_DISABLED_BACKENDS = ""; /// /// The default retry delay @@ -390,7 +390,7 @@ namespace Duplicati.Library.Main new CommandLineArgument("throttle-upload", CommandLineArgument.ArgumentType.Size, Strings.Options.ThrottleuploadShort, Strings.Options.ThrottleuploadLong, "0kb"), new CommandLineArgument("throttle-download", CommandLineArgument.ArgumentType.Size, Strings.Options.ThrottledownloadShort, Strings.Options.ThrottledownloadLong, "0kb"), new CommandLineArgument("throttle-disabled", CommandLineArgument.ArgumentType.Boolean, Strings.Options.DisablethrottleShort, Strings.Options.DisablethrottleLong, "false"), - new CommandLineArgument("throttle-disabled-backends", CommandLineArgument.ArgumentType.String, Strings.Options.DisablethrottlebackendsShort, Strings.Options.DisablethrottlebackendsLong("disable-throttle"), DEFAULT_THROTTLE_DISABLED_BACKENDS), + new CommandLineArgument("throttle-disabled-backends", CommandLineArgument.ArgumentType.String, Strings.Options.DisablethrottlebackendsShort, Strings.Options.DisablethrottlebackendsLong("throttle-disabled"), DEFAULT_THROTTLE_DISABLED_BACKENDS), new CommandLineArgument("skip-files-larger-than", CommandLineArgument.ArgumentType.Size, Strings.Options.SkipfileslargerthanShort, Strings.Options.SkipfileslargerthanLong), new CommandLineArgument("upload-unchanged-backups", CommandLineArgument.ArgumentType.Boolean, Strings.Options.UploadUnchangedBackupsShort, Strings.Options.UploadUnchangedBackupsLong, "false"), diff --git a/Duplicati/Library/Main/Strings.cs b/Duplicati/Library/Main/Strings.cs index b9ddad7a4..cb3f223cf 100644 --- a/Duplicati/Library/Main/Strings.cs +++ b/Duplicati/Library/Main/Strings.cs @@ -64,7 +64,7 @@ namespace Duplicati.Library.Main.Strings public static string ThrottleuploadShort { get { return LC.L(@"Max number of kilobytes to upload pr. second"); } } public static string DisablethrottleLong { get { return LC.L(@"Disable the throttling of upload and download speeds for this task. If there is a throttle set, it will be ignored when running this task."); } } public static string DisablethrottleShort { get { return LC.L(@"Disable throttling"); } } - public static string DisablethrottlebackendsLong(string option) { return LC.L(@"Disable the throttling of upload and download speeds for specific backends. If there is a throttle set, it will be ignore when running this task, if the backend is one of the specified backends. Multiple backends can be specified with a comma separator. This option has no effect if there is no throttle or if --{0} is set", option); } + public static string DisablethrottlebackendsLong(string option) { return LC.L(@"Disable the throttling of upload and download speeds for specific backends. If there is a throttle set, it will be ignored when running this task, if the backend is one of the specified backends. Multiple backends can be specified with a comma separator. This option has no effect if there is no throttle or if --{0} is set", option); } public static string DisablethrottlebackendsShort { get { return LC.L(@"Disable throttling for specific backends"); } } public static string NoencryptionLong { get { return LC.L(@"If you store the backups on a local disk, and prefer that they are kept unencrypted, you can turn of encryption completely by using this switch."); } } public static string NoencryptionShort { get { return LC.L(@"Disable encryption"); } } From 395a95b723f0139b29fd5da0cd3bccbc803a4464 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Sun, 29 Jun 2025 16:57:47 +0200 Subject: [PATCH 2/7] Configureable refresh token timeout This PR adds an option to define the timeout for the refresh token. The token is default set to 30 days. If the user interacts with Duplicati (i.e., loads the page) the refresh token is regenerated, meaning that the user is not asked to log in again unless the refresh token has expired fully. With this PR it is possible to configure this duration to be a shorter period, but not longer than the 30 days. --- Duplicati/Library/RestAPI/Strings.cs | 1 + Duplicati/Server/Program.cs | 2 ++ Duplicati/Server/WebServerLoader.cs | 16 ++++++++++++++++ Duplicati/WebserverCore/DuplicatiWebserver.cs | 5 +++++ 4 files changed, 24 insertions(+) diff --git a/Duplicati/Library/RestAPI/Strings.cs b/Duplicati/Library/RestAPI/Strings.cs index 8df084890..f9c3455cc 100644 --- a/Duplicati/Library/RestAPI/Strings.cs +++ b/Duplicati/Library/RestAPI/Strings.cs @@ -79,6 +79,7 @@ Error message: {0}", error); } public static string WebserverTimezoneDescription { get { return LC.L(@"The timezone to use for the webserver. The timezone must be a valid timezone identifier, such as ""America/New_York"" or ""UTC"". Common three-letter abbreviations like ""CET"" are supported, but ambiguous in some cases."); } } public static string WebserverPreAuthTokensDescription { get { return LC.L(@"A list of pre-authenticated tokens, separated with semicolons. These can be used in cases where the authentication is provided by a proxy. Each token must be at least 10 characters and not contain extended characters. The token must be provided by setting the header on each request to contain: Authentication: PreAuth "); } } public static string WebserverDisableApiExtensionsDescription { get { return LC.L(@"Disable the API extensions reported by the server. This will not disable the functionality, but will not report the extensions in the API responses. Use this option to degrade the server to a more basic API."); } } + public static string WebserverTokenDurationDescription { get { return LC.L(@"The duration of the refresh tokens issued by the server. This value indicates how much time can pass without activity before the user is asked to log in again. The value must be greater than zero and is capped at 30 days."); } } public static string DisabledbencryptionLong { get { return LC.L(@"Use this option to disable database encryption of sensitive fields"); } } public static string DisabledbencryptionShort { get { return LC.L(@"Disable database encryption"); } } public static string LogwindowseventlogLong { get { return LC.L(@"Use this option to log to the Windows event log. The provided name is in the format Log:Source. If no log name is provided, Duplicati is used."); } } diff --git a/Duplicati/Server/Program.cs b/Duplicati/Server/Program.cs index d8530f950..bea8dc8a4 100644 --- a/Duplicati/Server/Program.cs +++ b/Duplicati/Server/Program.cs @@ -367,6 +367,7 @@ namespace Duplicati.Server parsedOptions.Certificate, parsedOptions.AllowedHostnames, parsedOptions.DisableStaticFiles, + parsedOptions.TokenLifetimeInMinutes, parsedOptions.SPAPaths, parsedOptions.CorsOrigins, parsedOptions.PreAuthTokens @@ -949,6 +950,7 @@ namespace Duplicati.Server new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_TIMEZONE, CommandLineArgument.ArgumentType.String, Strings.Program.WebserverTimezoneDescription, Strings.Program.WebserverTimezoneDescription, TimeZoneHelper.GetLocalTimeZone(), null, TimeZoneHelper.GetTimeZones().Select(x => x.Id).ToArray()), new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_CORS_ORIGINS, CommandLineArgument.ArgumentType.Path, Strings.Program.WebserverCorsOriginsDescription, Strings.Program.WebserverCorsOriginsDescription, WebServerLoader.DEFAULT_OPTION_SPAPATHS), new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_PRE_AUTH_TOKENS, CommandLineArgument.ArgumentType.String, Strings.Program.WebserverPreAuthTokensDescription, Strings.Program.WebserverPreAuthTokensDescription), + new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_TOKENDURATION, CommandLineArgument.ArgumentType.Timespan, Strings.Program.WebserverTokenDurationDescription, Strings.Program.WebserverTokenDurationDescription, WebServerLoader.DEFAULT_OPTION_TOKENDURATION), new CommandLineArgument(PING_PONG_KEEPALIVE_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.Program.PingpongkeepaliveShort, Strings.Program.PingpongkeepaliveLong), new CommandLineArgument(DISABLE_UPDATE_CHECK_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.Program.DisableupdatecheckShort, Strings.Program.DisableupdatecheckLong), new CommandLineArgument(LOG_RETENTION_OPTION, CommandLineArgument.ArgumentType.Timespan, Strings.Program.LogretentionShort, Strings.Program.LogretentionLong, DEFAULT_LOG_RETENTION), diff --git a/Duplicati/Server/WebServerLoader.cs b/Duplicati/Server/WebServerLoader.cs index 31f687c39..49377e06e 100644 --- a/Duplicati/Server/WebServerLoader.cs +++ b/Duplicati/Server/WebServerLoader.cs @@ -106,6 +106,11 @@ public static class WebServerLoader /// public const string OPTION_WEBSERVICE_PRE_AUTH_TOKENS = "webservice-pre-auth-tokens"; + /// + /// Option for setting the webservice token duration + /// + public const string OPTION_WEBSERVICE_TOKENDURATION = "webservice-token-duration"; + /// /// The default path to the web root /// @@ -121,6 +126,11 @@ public static class WebServerLoader /// public const int DEFAULT_OPTION_PORT = 8200; + /// + /// The default token duration for JWT tokens + /// + public const string DEFAULT_OPTION_TOKENDURATION = "30D"; + /// /// Option for setting if to use HTTPS /// @@ -160,6 +170,7 @@ public static class WebServerLoader /// SSL certificate, if any /// The allowed hostnames /// If static files should be disabled + /// The lifetime of refresh tokens in minutes /// The paths to serve as SPAs /// The origins to allow for CORS public record ParsedWebserverSettings( @@ -169,6 +180,7 @@ public static class WebServerLoader X509Certificate2Collection? Certificate, IEnumerable AllowedHostnames, bool DisableStaticFiles, + int TokenLifetimeInMinutes, IEnumerable SPAPaths, IEnumerable CorsOrigins, IReadOnlySet PreAuthTokens @@ -257,6 +269,9 @@ public static class WebServerLoader if (string.IsNullOrWhiteSpace(spaPathsString)) spaPathsString = DEFAULT_OPTION_SPAPATHS; + var duration = Utility.ParseTimespanOption(options, OPTION_WEBSERVICE_TOKENDURATION, DEFAULT_OPTION_TOKENDURATION); + var tokenLifetimeInMinutes = (int)Math.Ceiling(duration.TotalMinutes); + var settings = new ParsedWebserverSettings( webroot, -1, @@ -264,6 +279,7 @@ public static class WebServerLoader connection.ApplicationSettings.UseHTTPS ? connection.ApplicationSettings.ServerSSLCertificate : null, (connection.ApplicationSettings.AllowedHostnames ?? string.Empty).Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries), Utility.ParseBoolOption(options, OPTION_WEBSERVICE_API_ONLY), + tokenLifetimeInMinutes, spaPathsString.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries), options.GetValueOrDefault(OPTION_WEBSERVICE_CORS_ORIGINS)?.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries) ?? Enumerable.Empty(), preAuthTokens diff --git a/Duplicati/WebserverCore/DuplicatiWebserver.cs b/Duplicati/WebserverCore/DuplicatiWebserver.cs index ccaf2bcf6..158d6c69f 100644 --- a/Duplicati/WebserverCore/DuplicatiWebserver.cs +++ b/Duplicati/WebserverCore/DuplicatiWebserver.cs @@ -110,6 +110,7 @@ public class DuplicatiWebserver /// The certificate, if using SSL /// The allowed hostnames /// If static files should be disabled + /// The lifetime of refresh tokens in minutes /// The paths to serve as SPAs /// The origins to allow for CORS /// The pre-authenticated tokens @@ -120,6 +121,7 @@ public class DuplicatiWebserver X509Certificate2Collection? Certificate, IEnumerable AllowedHostnames, bool DisableStaticFiles, + int TokenLifetimeInMinutes, IEnumerable SPAPaths, IEnumerable CorsOrigins, IReadOnlySet PreAuthTokens @@ -193,6 +195,9 @@ public class DuplicatiWebserver var jwtConfig = JsonSerializer.Deserialize(connection.ApplicationSettings.JWTConfig) ?? throw new Exception("Failed to deserialize JWTConfig"); + if (settings.TokenLifetimeInMinutes > 0) + jwtConfig = jwtConfig with { RefreshTokenDurationInMinutes = Math.Min(settings.TokenLifetimeInMinutes, 60 * 24 * 30) }; + if (EnableSwagger) { // Swagger is picking up JSON settings from controllers, even though we do not use controllers From ccbf3a155215201aadbbe76c639a642ae008571b Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Tue, 1 Jul 2025 11:43:38 +0200 Subject: [PATCH 3/7] Default to disable snapshots when testing --- Duplicati/UnitTest/BasicSetupHelper.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Duplicati/UnitTest/BasicSetupHelper.cs b/Duplicati/UnitTest/BasicSetupHelper.cs index 0fc14097b..251167b20 100644 --- a/Duplicati/UnitTest/BasicSetupHelper.cs +++ b/Duplicati/UnitTest/BasicSetupHelper.cs @@ -193,6 +193,7 @@ namespace Duplicati.UnitTest opts["blocksize"] = "10kb"; opts["backup-test-samples"] = "0"; opts["unittest-mode"] = "true"; + opts["snapshot-policy"] = "Off"; return opts; } From 9eb2ceada5e31f2c4bc9fa372a8622efaad67398 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Tue, 1 Jul 2025 11:45:29 +0200 Subject: [PATCH 4/7] Update permission check to drop privilege This PR adjusts the check for SeBackupPrivilege and unassigns the privilege after testing if it could be enabled. This fixes #6317 --- Duplicati/Library/Utility/PermissionHelper.cs | 63 ++++++++++++++----- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/Duplicati/Library/Utility/PermissionHelper.cs b/Duplicati/Library/Utility/PermissionHelper.cs index e27a6bb4d..0b20a5c8c 100644 --- a/Duplicati/Library/Utility/PermissionHelper.cs +++ b/Duplicati/Library/Utility/PermissionHelper.cs @@ -124,6 +124,10 @@ public static class PermissionHelper /// Constant for privilege enabled attribute. /// const int SE_PRIVILEGE_ENABLED = 0x00000002; + /// + /// The TOKEN_INFORMATION_CLASS.TokenPrivileges value + /// + const int TOKEN_INFORMATION_TOKEN_PRIVILEGES = 3; /// /// Structure that contains a locally unique identifier (LUID). @@ -244,7 +248,7 @@ public static class PermissionHelper ref TOKEN_PRIVILEGES NewState, int BufferLength, IntPtr PreviousState, - IntPtr ReturnLength + out uint ReturnLength ); /// @@ -254,6 +258,15 @@ public static class PermissionHelper [DllImport("kernel32.dll")] static extern uint GetLastError(); + /// + /// Closes an open object handle. + /// + /// The handle to the object to be closed. + /// true if the function succeeds; otherwise, false. + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CloseHandle(IntPtr hObject); + /// /// Flag tracking if backup privilege is enabled /// @@ -272,37 +285,59 @@ public static class PermissionHelper if (_isPrivilegeEnabled.HasValue) return _isPrivilegeEnabled.Value; + _isPrivilegeEnabled = false; + + // Open the primary token for this process + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out var hTok)) + return false; + try { - _isPrivilegeEnabled = false; - if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out IntPtr tokenHandle)) - return false; - - if (!LookupPrivilegeValue(null, privilegeName, out LUID luid)) + // Translate the textual privilege name (e.g. "SeBackupPrivilege") to a LUID + if (!LookupPrivilegeValue(null, privilegeName, out var luid)) return false; + // Build TOKEN_PRIVILEGES {1 entry, SE_PRIVILEGE_ENABLED} var tp = new TOKEN_PRIVILEGES { PrivilegeCount = 1, - Privileges = new LUID_AND_ATTRIBUTES() + Privileges = new LUID_AND_ATTRIBUTES { Luid = luid, Attributes = SE_PRIVILEGE_ENABLED } }; - if (!AdjustTokenPrivileges(tokenHandle, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero)) - return false; + // Query the size of the current privileges set + GetTokenInformation(hTok, TOKEN_INFORMATION_TOKEN_PRIVILEGES, IntPtr.Zero, 0, out int requiredLen); + + // Allocate a buffer of the requested size + var prevStatePtr = Marshal.AllocHGlobal(requiredLen); + + try + { + // Enable the privilege, capturing the previous state + if (!AdjustTokenPrivileges(hTok, false, ref tp, requiredLen, prevStatePtr, out var _)) + return false; + + // If GetLastError() == ERROR_SUCCESS (0), the privilege is now enabled + _isPrivilegeEnabled = Marshal.GetLastWin32Error() == 0; + + // Restore the original state + var oldState = Marshal.PtrToStructure(prevStatePtr); + AdjustTokenPrivileges(hTok, false, ref oldState, 0, IntPtr.Zero, out var _); + } + finally + { + Marshal.FreeHGlobal(prevStatePtr); + } - _isPrivilegeEnabled = (GetLastError() == 0); // If not zero, then the privilege wasn't assigned (e.g., ERROR_NOT_ALL_ASSIGNED) return _isPrivilegeEnabled.Value; } - catch (Exception) + finally { - // Ignore this error + CloseHandle(hTok); } - - return _isPrivilegeEnabled ?? false; } /// From 9d8f426c999e1645b2b9d10f2be688ed323f7303 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Tue, 1 Jul 2025 11:47:30 +0200 Subject: [PATCH 5/7] Fixed crashs in debug mode This fixes the callstack walker to handle cases where the call frame has no assembly. The code is only compiled for debug builds. --- Duplicati/Library/Utility/TempFile.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Duplicati/Library/Utility/TempFile.cs b/Duplicati/Library/Utility/TempFile.cs index aa9853ea0..cc1f1a8ee 100644 --- a/Duplicati/Library/Utility/TempFile.cs +++ b/Duplicati/Library/Utility/TempFile.cs @@ -57,7 +57,9 @@ namespace Duplicati.Library.Utility { var st = new System.Diagnostics.StackTrace(); foreach (var f in st.GetFrames()) - if (f.GetMethod().DeclaringType.Assembly != typeof(TempFile).Assembly) + { + var asm = f.GetMethod()?.DeclaringType?.Assembly; + if (asm != null && asm != typeof(TempFile).Assembly) { var n = string.Format("{0}_{1}_{2}_{3}", f.GetMethod().DeclaringType.FullName, f.GetMethod().Name, Library.Utility.Utility.SerializeDateTime(DateTime.UtcNow), Guid.NewGuid().ToString().Substring(0, 8)); if (n.IndexOfAny(System.IO.Path.GetInvalidFileNameChars()) >= 0) @@ -69,6 +71,7 @@ namespace Duplicati.Library.Utility return n; } } + } var s = Guid.NewGuid().ToString(); lock (m_lock) From 8ed3fa5d157f9b7217284ac92587424a8a106c86 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Tue, 1 Jul 2025 11:49:59 +0200 Subject: [PATCH 6/7] Removed limitation of token duration --- Duplicati/WebserverCore/DuplicatiWebserver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Duplicati/WebserverCore/DuplicatiWebserver.cs b/Duplicati/WebserverCore/DuplicatiWebserver.cs index 158d6c69f..c02dc4c93 100644 --- a/Duplicati/WebserverCore/DuplicatiWebserver.cs +++ b/Duplicati/WebserverCore/DuplicatiWebserver.cs @@ -196,7 +196,7 @@ public class DuplicatiWebserver ?? throw new Exception("Failed to deserialize JWTConfig"); if (settings.TokenLifetimeInMinutes > 0) - jwtConfig = jwtConfig with { RefreshTokenDurationInMinutes = Math.Min(settings.TokenLifetimeInMinutes, 60 * 24 * 30) }; + jwtConfig = jwtConfig with { RefreshTokenDurationInMinutes = settings.TokenLifetimeInMinutes }; if (EnableSwagger) { From b7dc8e81fe6142408446aa655addf6a08e88b262 Mon Sep 17 00:00:00 2001 From: "Marcelo C." Date: Tue, 1 Jul 2025 09:41:31 -0300 Subject: [PATCH 7/7] Fixes typo and improves argument handling Ensures the unmodified command-line arguments are passed to the help detect function. Related to https://github.com/duplicati/duplicati/pull/5889#issuecomment-3020522797 --- .../GUI/Duplicati.GUI.TrayIcon/Program.cs | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs index b16ccfad0..f2386d62e 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.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 +// +// 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 +// +// 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 +// +// 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; @@ -100,7 +100,7 @@ namespace Duplicati.GUI.TrayIcon if (OperatingSystem.IsWindows() && !Utility.ParseBoolOption(options, DETACHED_PROCESS)) Win32.AttachConsole(Win32.ATTACH_PARENT_PROCESS); - if (HelpOptionExtensions.IsArgumentAnyHelpString(args)) + if (HelpOptionExtensions.IsArgumentAnyHelpString(_args)) { Console.WriteLine("Supported commandline arguments:"); Console.WriteLine(); @@ -165,7 +165,7 @@ namespace Duplicati.GUI.TrayIcon throw; } - // We have a hosted server, if this is the first run, + // We have a hosted server, if this is the first run, // we should open the main page var connection = Server.Program.DuplicatiWebserver.Provider.GetRequiredService(); openui = connection.ApplicationSettings.IsFirstRun || connection.ApplicationSettings.ServerPortChanged; @@ -281,7 +281,7 @@ No password provided, unable to connect to server, exiting"); // If the server shuts down, shut down the tray-icon as well Action shutdownEvent = () => { - // Make sure we do not start again after + // Make sure we do not start again after // a controlled exit reSpawn = 100; tk.InvokeExit();