Merge pull request #5954 from duplicati/feature/improve-trayicon
Improved TrayIcon detached.
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -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;
|
||||
@@ -40,7 +42,7 @@ namespace Duplicati.GUI.TrayIcon
|
||||
public class AvaloniaRunner : TrayIconBase
|
||||
{
|
||||
private static readonly string LOGTAG = Log.LogTagFromType<AvaloniaRunner>();
|
||||
private AvaloniaApp application;
|
||||
private AvaloniaApp? application;
|
||||
private ProcessBasedActionDelay actionDelayer = new ProcessBasedActionDelay();
|
||||
private IEnumerable<AvaloniaMenuItem> menuItems = Enumerable.Empty<AvaloniaMenuItem>();
|
||||
|
||||
@@ -106,6 +108,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 +118,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,7 +168,7 @@ 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)
|
||||
{
|
||||
//var icon = Win32NativeNotifyIcon.InfoFlags.NIIF_INFO;
|
||||
|
||||
@@ -180,8 +184,6 @@ namespace Duplicati.GUI.TrayIcon
|
||||
//icon = Win32NativeNotifyIcon.InfoFlags.NIIF_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
//m_ntfIcon.ShowBalloonTip(title, message, icon);
|
||||
}
|
||||
|
||||
protected override void Exit()
|
||||
@@ -209,7 +211,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 +239,12 @@ namespace Duplicati.GUI.TrayIcon
|
||||
|
||||
public string Text { get; private set; }
|
||||
public Action Callback { get; private set; }
|
||||
public IList<IMenuItem> SubItems { get; private set; }
|
||||
public IList<IMenuItem>? 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<IMenuItem> subitems)
|
||||
@@ -278,6 +281,9 @@ namespace Duplicati.GUI.TrayIcon
|
||||
/// </summary>
|
||||
private void UpdateIcon()
|
||||
{
|
||||
if (nativeMenuItem == null)
|
||||
return;
|
||||
|
||||
nativeMenuItem.Icon = this.Icon switch
|
||||
{
|
||||
MenuIcons.Status => AvaloniaRunner.LoadBitmap("context-menu-open.png"),
|
||||
@@ -301,6 +307,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 +338,9 @@ namespace Duplicati.GUI.TrayIcon
|
||||
|
||||
public class AvaloniaApp : Application
|
||||
{
|
||||
private Avalonia.Controls.TrayIcon trayIcon;
|
||||
private Avalonia.Controls.TrayIcon? trayIcon;
|
||||
|
||||
private List<AvaloniaMenuItem> menuItems;
|
||||
private List<AvaloniaMenuItem>? menuItems;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -355,13 +369,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<AvaloniaMenuItem> menuItems)
|
||||
@@ -413,10 +431,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)}");
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ServerStatusImpl>("GET", $"/serverstate{query}", null, longpoll ? Library.Utility.Timeparser.ParseTimeSpan(LONGPOLL_TIMEOUT) : null);
|
||||
m_status = await PerformRequestAsync<ServerStatusImpl>("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<NotificationImpl[]>("GET", "/notifications", null, null);
|
||||
var notifications = await PerformRequestAsync<NotificationImpl[]>("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<Dictionary<string, string>>("GET", "/serversettings", null, null);
|
||||
var settings = await PerformRequestAsync<Dictionary<string, string>>("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<string>(req.Method, req.Endpoint, req.Body, req.Timeout);
|
||||
PerformRequestAsync<string>(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<T>(string method, string urlfragment, string body, TimeSpan? timeout)
|
||||
private async Task<T> PerformRequestAsync<T>(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<T>(method, urlfragment, body, timeout);
|
||||
return await PerformRequestInternalAsync<T>(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,49 @@ 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();
|
||||
// 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<IJWTTokenProvider>();
|
||||
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<SigninResponse>("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;
|
||||
m_accesstoken = PerformRequestInternal<SigninResponse>("POST", "/auth/signin", JsonSerializer.Serialize(new { SigninToken = token }), null).AccessToken;
|
||||
|
||||
// Use the signin token to get an access token
|
||||
m_accesstoken = (await PerformRequestInternalAsync<SigninResponse>("POST", "/auth/signin", JsonSerializer.Serialize(new { SigninToken = token }), null).ConfigureAwait(false)).AccessToken;
|
||||
}
|
||||
|
||||
private async Task<T> PerformRequestInternalAsync<T>(string method, string endpoint, string body, TimeSpan? timeout)
|
||||
@@ -358,13 +390,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<T>(stream, serializerOptions).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private T PerformRequestInternal<T>(string method, string endpoint, string body, TimeSpan? timeout)
|
||||
=> PerformRequestInternalAsync<T>(method, endpoint, body, timeout).Await();
|
||||
|
||||
private void ExecuteAndNotify(string method, string urifragment, string body)
|
||||
{
|
||||
lock (m_lock)
|
||||
@@ -391,21 +420,29 @@ namespace Duplicati.GUI.TrayIcon
|
||||
}
|
||||
|
||||
private sealed record SigninTokenResponse(string Token);
|
||||
private string IssueSigninToken(string password)
|
||||
=> PerformRequest<SigninTokenResponse>("POST", "/auth/issuesignintoken", JsonSerializer.Serialize(new { Password = password }), null).Token;
|
||||
private async Task<string> IssueSigninTokenAsync(string password)
|
||||
=> (await PerformRequestInternalAsync<SigninTokenResponse>("POST", "/auth/issuesignintoken", JsonSerializer.Serialize(new { Password = password }), null).ConfigureAwait(false)).Token;
|
||||
|
||||
private string ObtainSignInToken()
|
||||
private async Task<string> ObtainSignInTokenAsync()
|
||||
{
|
||||
string signinjwt = null;
|
||||
|
||||
// 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<IJWTTokenProvider>().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<JWTConfig>(cfg)).CreateSigninToken("trayicon");
|
||||
@@ -413,20 +450,28 @@ 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<string> GetStatusWindowURLAsync()
|
||||
{
|
||||
get
|
||||
string signinjwt = null;
|
||||
if (!m_disableTrayIconLogin)
|
||||
{
|
||||
var signinjwt = m_disableTrayIconLogin ? null : ObtainSignInToken();
|
||||
return string.IsNullOrWhiteSpace(signinjwt)
|
||||
? m_baseUri + STATUS_WINDOW
|
||||
: m_baseUri + SIGNIN_WINDOW + $"?token={signinjwt}";
|
||||
try
|
||||
{
|
||||
signinjwt = await ObtainSignInTokenAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(signinjwt)
|
||||
? m_baseUri + STATUS_WINDOW
|
||||
: m_baseUri + SIGNIN_WINDOW + $"?token={signinjwt}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<IMenuItem> 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<IMenuItem> 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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ namespace Duplicati.Server
|
||||
/// </summary>
|
||||
public List<Runner.IRunnerData> 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() ?? []; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
+54
-17
@@ -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;
|
||||
@@ -265,7 +268,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));
|
||||
|
||||
@@ -311,6 +314,24 @@ 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");
|
||||
if (OperatingSystem.IsWindows())
|
||||
Environment.FailFast("Cancel key pressed twice, terminating now");
|
||||
else
|
||||
Environment.Exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
ServerStartedEvent.Set();
|
||||
ApplicationExitEvent.WaitOne();
|
||||
}
|
||||
@@ -668,30 +689,45 @@ namespace Duplicati.Server
|
||||
private static async Task ApplySecretProvider(Dictionary<string, string> 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<string, string> 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))
|
||||
@@ -942,8 +978,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),
|
||||
|
||||
Reference in New Issue
Block a user