2025-01-17 14:00:14 -03:00
// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
2025-07-01 09:41:31 -03:00
//
// 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
2025-01-17 14:00:14 -03:00
// Software is furnished to do so, subject to the following conditions:
2025-07-01 09:41:31 -03:00
//
// The above copyright notice and this permission notice shall be included in
2025-01-17 14:00:14 -03:00
// all copies or substantial portions of the Software.
2025-07-01 09:41:31 -03:00
//
// 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
2024-02-28 15:45:30 +01:00
// DEALINGS IN THE SOFTWARE.
2022-08-03 12:35:43 -07:00
using System ;
2011-06-06 18:48:52 +00:00
using System.Collections.Generic ;
2024-08-30 13:08:37 +02:00
using System.IO ;
2024-12-05 10:34:10 +01:00
using System.Linq ;
2017-05-02 21:14:28 +02:00
using System.Net ;
2025-02-08 17:30:27 +01:00
using System.Net.Http ;
using System.Threading ;
2025-03-25 10:56:01 +01:00
using CoCoL ;
using Duplicati.CommandLine ;
2025-01-28 13:01:35 +01:00
using Duplicati.Library.AutoUpdater ;
2017-06-29 00:08:02 +02:00
using Duplicati.Library.Interface ;
2025-01-17 14:00:14 -03:00
using Duplicati.Library.Utility ;
2024-06-19 13:54:10 +02:00
using Duplicati.Server ;
2025-05-05 16:13:46 +02:00
using Duplicati.Server.Database ;
using Duplicati.WebserverCore.Services ;
using Microsoft.Extensions.DependencyInjection ;
2025-01-17 14:00:14 -03:00
using Uri = System . Uri ;
2011-06-06 18:48:52 +00:00
namespace Duplicati.GUI.TrayIcon
{
2011-11-28 11:05:42 +00:00
public static class Program
2011-06-06 18:48:52 +00:00
{
2024-11-26 11:26:50 +01:00
/// <summary>
/// The source of the password
/// </summary>
2018-02-25 13:54:20 +01:00
public enum PasswordSource
{
2024-11-26 11:26:50 +01:00
/// <summary>
/// No password, using token information from the database
/// </summary>
2018-02-25 13:54:20 +01:00
Database ,
2024-11-26 11:26:50 +01:00
/// <summary>
/// No password supplied, using the hosted server
/// </summary>
HostedServer ,
/// <summary>
/// The password was supplied on the commandline
/// </summary>
SuppliedPassword
2018-02-25 13:54:20 +01:00
}
2011-11-21 22:28:20 +00:00
public static HttpServerConnection Connection ;
2024-06-07 15:56:43 +02:00
2012-07-14 10:42:48 +00:00
private const string HOSTURL_OPTION = "hosturl" ;
private const string NOHOSTEDSERVER_OPTION = "no-hosted-server" ;
2017-05-02 21:14:28 +02:00
private const string READCONFIGFROMDB_OPTION = "read-config-from-db" ;
2024-10-29 13:28:44 +01:00
private const string ACCEPTED_SSL_CERTIFICATE = "host-cert-hash" ;
2017-05-02 21:14:28 +02:00
2017-10-11 08:57:12 +02:00
private const string DETACHED_PROCESS = "detached-process" ;
2014-03-27 15:24:38 +01:00
private const string BROWSER_COMMAND_OPTION = "browser-command" ;
2025-03-25 11:36:44 +01:00
private static readonly IReadOnlySet < string > DETATCHED_WEBERVER_OPTIONS = new HashSet < string >( StringComparer . OrdinalIgnoreCase )
{
WebServerLoader . OPTION_WEBSERVICE_PASSWORD ,
WebServerLoader . OPTION_PORT ,
DataFolderManager . SERVER_DATAFOLDER_OPTION ,
DataFolderManager . PORTABLE_MODE_OPTION
};
2012-07-14 10:42:48 +00:00
2025-03-25 11:13:57 +01:00
private static string DEFAULT_HOSTURL => $"http://{Utility.IpVersionCompatibleLoopback}:8200" ;
2024-06-07 15:56:43 +02:00
2014-03-27 15:24:38 +01:00
private static string _browser_command = null ;
2019-02-12 21:50:33 +01:00
private static bool disableTrayIconLogin = false ;
private static bool openui = false ;
2025-03-11 16:24:15 -03:00
private static Uri serverURL = new ( DEFAULT_HOSTURL );
2014-03-27 15:24:38 +01:00
public static string BrowserCommand { get { return _browser_command ; } }
2017-05-02 21:39:04 +02:00
public static Server . Database . Connection databaseConnection = null ;
2017-05-02 21:14:28 +02:00
2011-06-06 18:48:52 +00:00
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
2024-03-15 14:18:56 +01:00
public static int Main ( string [] _args )
2011-06-06 18:48:52 +00:00
{
2025-03-25 11:13:57 +01:00
PreloadSettingsLoader . ConfigurePreloadSettings ( ref _args , PackageHelper . NamedExecutable . TrayIcon );
var args = new List < string >( _args );
var options = CommandLineParser . ExtractOptions ( args );
2014-04-02 11:34:33 +02:00
2025-03-25 11:13:57 +01:00
if ( OperatingSystem . IsWindows () && ! Utility . ParseBoolOption ( options , DETACHED_PROCESS ))
Win32 . AttachConsole ( Win32 . ATTACH_PARENT_PROCESS );
2025-02-08 17:30:27 +01:00
2025-07-01 09:41:31 -03:00
if ( HelpOptionExtensions . IsArgumentAnyHelpString ( _args ))
2019-08-03 22:14:41 -04:00
{
Console . WriteLine ( "Supported commandline arguments:" );
Console . WriteLine ();
2014-04-02 11:34:33 +02:00
2025-03-25 11:13:57 +01:00
foreach ( ICommandLineArgument arg in AllSupportedCommands )
2019-08-03 22:14:41 -04:00
Console . WriteLine ( "--{0}: {1}" , arg . Name , arg . LongDescription );
2014-04-02 11:34:33 +02:00
2019-08-03 22:14:41 -04:00
Console . WriteLine ( "Additionally, these server options are also supported:" );
Console . WriteLine ();
2014-04-02 11:34:33 +02:00
2025-03-25 11:13:57 +01:00
foreach ( ICommandLineArgument arg in WebserverSupportedCommands )
2019-08-03 22:14:41 -04:00
Console . WriteLine ( "--{0}: {1}" , arg . Name , arg . LongDescription );
2017-06-22 15:10:31 +02:00
2024-03-15 14:18:56 +01:00
return 0 ;
2019-08-03 22:14:41 -04:00
}
2019-02-12 21:47:17 +01:00
2025-07-02 20:42:35 +02:00
// Apply secret provider, if any, so password etc can be retrieved
Library . Main . SecretProviderHelper . ApplySecretProviderAsync ([], [], options , TempFolder . SystemTempPath , null , CancellationToken . None )
. Await ();
2019-08-03 22:14:41 -04:00
options . TryGetValue ( BROWSER_COMMAND_OPTION , out _browser_command );
2019-08-03 22:08:17 -04:00
2019-08-03 22:14:41 -04:00
HostedInstanceKeeper hosted = null ;
string password = null ;
2024-11-26 11:26:50 +01:00
var passwordSource = PasswordSource . SuppliedPassword ;
2024-10-29 13:28:44 +01:00
var acceptedHostCertificate = options . GetValueOrDefault ( ACCEPTED_SSL_CERTIFICATE , null );
2025-03-25 11:13:57 +01:00
var detached = Utility . ParseBoolOption ( options , NOHOSTEDSERVER_OPTION );
var supportedCommands = BasicSupportedCommands . AsEnumerable ();
if ( detached )
{
supportedCommands = DetachSupportedCommands
2025-03-25 11:36:44 +01:00
. Concat ( supportedCommands );
2025-03-25 11:13:57 +01:00
}
else
{
supportedCommands = supportedCommands . Concat ( WebserverSupportedCommands );
}
// Validate options, and log to console
using ( var logger = new ConsoleOutput ( Console . Out , options ))
2025-03-25 11:36:44 +01:00
CommandLineArgumentValidator . ValidateArguments ( supportedCommands , options , Server . Program . KnownDuplicateOptions , new HashSet < string >());
2025-03-25 11:13:57 +01:00
if (! detached )
2019-08-03 22:14:41 -04:00
{
try
{
2024-09-07 08:25:16 +02:00
// Tell the hosted server it was started by the TrayIcon
2025-06-25 15:24:04 -03:00
var applicationSettings = new ApplicationSettings
{
Origin = "Tray icon"
};
2024-11-26 11:26:50 +01:00
passwordSource = PasswordSource . HostedServer ;
2025-03-25 11:36:44 +01:00
// Ignore TrayIcon specific settings
foreach ( var c in BasicSupportedCommands . Select ( x => x . Name ))
Server . Program . ValidationIgnoredOptions . Add ( c );
2025-05-05 16:13:46 +02:00
hosted = new HostedInstanceKeeper ( applicationSettings , _args );
2014-03-21 10:34:34 +01:00
}
2024-10-03 10:05:15 +02:00
catch ( Exception ex )
2019-08-03 22:14:41 -04:00
{
2024-10-03 10:05:15 +02:00
if ( ex . InnerException != null && ex . InnerException is Server . SingleInstance . MultipleInstanceException )
{
Console . WriteLine ( Server . Strings . Program . AnotherInstanceDetected );
return 1 ;
}
else
throw ;
2019-08-03 22:14:41 -04:00
}
2024-10-03 16:33:46 +02:00
2025-07-01 09:41:31 -03:00
// We have a hosted server, if this is the first run,
2019-08-03 22:14:41 -04:00
// we should open the main page
2025-05-05 16:13:46 +02:00
var connection = Server . Program . DuplicatiWebserver . Provider . GetRequiredService < Connection >();
openui = connection . ApplicationSettings . IsFirstRun || connection . ApplicationSettings . ServerPortChanged ;
2018-02-25 13:54:20 +01:00
2025-05-05 16:13:46 +02:00
var scheme = connection . ApplicationSettings . UseHTTPS ? "https" : "http" ;
2024-10-29 13:55:12 +01:00
serverURL = new UriBuilder ( serverURL )
2018-02-25 13:54:20 +01:00
{
2025-05-05 16:13:46 +02:00
Port = Server . Program . DuplicatiWebserver . Port ,
2019-08-03 22:14:41 -04:00
Scheme = scheme
2024-10-29 13:55:12 +01:00
}. Uri ;
2024-10-29 13:28:44 +01:00
2025-05-05 16:13:46 +02:00
if ( connection . ApplicationSettings . UseHTTPS && string . IsNullOrWhiteSpace ( acceptedHostCertificate ))
acceptedHostCertificate = connection . ApplicationSettings . ServerSSLCertificate ?. FirstOrDefault ( x => x . HasPrivateKey )?. GetCertHashString ();
2024-10-29 13:28:44 +01:00
2019-08-03 22:14:41 -04:00
}
2025-03-25 11:13:57 +01:00
else if ( Utility . ParseBoolOption ( options , READCONFIGFROMDB_OPTION ))
2019-08-03 22:14:41 -04:00
{
2025-04-02 10:15:21 -03:00
if ( File . Exists ( Path . Combine ( DataFolderManager . GetDataFolder ( DataFolderManager . AccessMode . ReadWritePermissionSet ), DataFolderManager . SERVER_DATABASE_FILENAME )))
2019-08-03 22:14:41 -04:00
{
2024-11-26 11:26:50 +01:00
passwordSource = PasswordSource . Database ;
2025-07-10 21:03:16 +02:00
databaseConnection = Server . Program . GetDatabaseConnection ( new ApplicationSettings (), options , true , false );
2024-08-30 13:08:37 +02:00
if ( databaseConnection != null )
{
disableTrayIconLogin = databaseConnection . ApplicationSettings . DisableTrayIconLogin ;
2024-10-29 13:28:44 +01:00
if ( databaseConnection . ApplicationSettings . UseHTTPS && string . IsNullOrWhiteSpace ( acceptedHostCertificate ))
2024-12-05 10:34:10 +01:00
acceptedHostCertificate = databaseConnection . ApplicationSettings . ServerSSLCertificate ?. FirstOrDefault ( x => x . HasPrivateKey )?. GetCertHashString ();
2017-06-29 22:47:21 +02:00
2024-10-29 11:15:06 +01:00
var scheme = databaseConnection . ApplicationSettings . UseHTTPS ? "https" : "http" ;
2024-08-30 13:08:37 +02:00
serverURL = new UriBuilder ( serverURL )
{
Port = databaseConnection . ApplicationSettings . LastWebserverPort == - 1 ? serverURL . Port : databaseConnection . ApplicationSettings . LastWebserverPort ,
Scheme = scheme
}. Uri ;
}
2019-02-11 21:35:59 +01:00
}
2019-08-03 22:14:41 -04:00
}
2019-08-03 22:08:17 -04:00
2024-06-19 13:54:10 +02:00
// Legacy undocumented way to provide the password
if ( options . TryGetValue ( "webserver-password" , out var pwd ))
password = pwd ;
if ( options . TryGetValue ( WebServerLoader . OPTION_WEBSERVICE_PASSWORD , out pwd ))
2019-08-03 22:14:41 -04:00
password = pwd ;
2019-02-12 21:47:17 +01:00
2024-08-20 12:15:46 +02:00
// Let the user specify the port, if they are not providing a hosturl
if (! options . ContainsKey ( HOSTURL_OPTION ) && options . TryGetValue ( WebServerLoader . OPTION_PORT , out var portString ) && int . TryParse ( portString , out var port ))
serverURL = new UriBuilder ( serverURL ) { Port = port }. Uri ;
2024-06-19 13:54:10 +02:00
if ( options . TryGetValue ( HOSTURL_OPTION , out var url ))
2019-08-03 22:14:41 -04:00
serverURL = new Uri ( url );
2019-08-03 22:08:17 -04:00
2024-11-26 11:26:50 +01:00
if ( string . IsNullOrWhiteSpace ( password ) && passwordSource == PasswordSource . SuppliedPassword )
2024-08-20 12:17:04 +02:00
{
Console . WriteLine ( $@"
When running the TrayIcon without a hosted server, you must provide the server password via the option --{WebServerLoader.OPTION_WEBSERVICE_PASSWORD}=<password>.
If the TrayIcon instance has read access to the server database, you can also or use the option --{READCONFIGFROMDB_OPTION}, possibly with --server-datafolder=<path>.
No password provided, unable to connect to server, exiting" );
return 1 ;
}
2024-11-26 11:26:50 +01:00
StartTray ( _args , options , hosted , passwordSource , password , acceptedHostCertificate );
2024-03-15 14:18:56 +01:00
return 0 ;
2019-02-12 21:47:17 +01:00
}
2024-11-26 11:26:50 +01:00
private static void StartTray ( string [] _args , Dictionary < string , string > options , HostedInstanceKeeper hosted , PasswordSource passwordSource , string password , string acceptedHostCertificate )
2019-02-12 21:47:17 +01:00
{
2019-08-03 22:14:41 -04:00
using ( hosted )
2011-11-21 22:28:20 +00:00
{
2019-08-03 22:14:41 -04:00
var reSpawn = 0 ;
2017-06-28 23:45:56 +02:00
2019-08-03 22:14:41 -04:00
do
{
2025-02-08 17:30:27 +01:00
if ( reSpawn > 0 )
Thread . Sleep ( 1000 );
2019-08-03 22:14:41 -04:00
try
2019-08-03 22:08:17 -04:00
{
2024-04-24 21:17:38 +02:00
ServicePointManager . SecurityProtocol = SecurityProtocolType . SystemDefault ;
2025-07-02 20:42:35 +02:00
2025-07-08 13:03:33 +02:00
using ( Connection = new HttpServerConnection ( hosted ?. applicationSettings , serverURL , password , passwordSource , disableTrayIconLogin , acceptedHostCertificate , options ))
2019-08-03 22:14:41 -04:00
{
2025-03-25 10:40:45 +01:00
// Make sure we have the latest status, but don't care if it fails
Connection . UpdateStatus (). FireAndForget ();
2025-02-08 17:30:27 +01:00
2024-03-04 14:34:38 +01:00
using ( var tk = RunTrayIcon ())
2019-08-03 22:08:17 -04:00
{
2025-06-25 15:24:04 -03:00
if ( Server . Program . ApplicationInstance != null )
2019-08-03 22:14:41 -04:00
Server . Program . ApplicationInstance . SecondInstanceDetected +=
2025-06-25 15:24:04 -03:00
x => tk . ShowStatusWindow ();
2019-08-03 22:14:41 -04:00
// TODO: If we change to hosted browser this should be a callback
if ( openui )
2017-05-02 21:14:28 +02:00
{
2025-02-08 17:30:27 +01:00
Connection . GetStatusWindowURLAsync (). ContinueWith ( t =>
2019-08-03 22:14:41 -04:00
{
2025-02-08 17:30:27 +01:00
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 );
2025-05-05 16:13:46 +02:00
var connection = Server . Program . DuplicatiWebserver . Provider . GetRequiredService < Connection >();
connection . ApplicationSettings . IsFirstRun = false ;
connection . ApplicationSettings . ServerPortChanged = false ;
2025-02-08 17:30:27 +01:00
});
2019-08-03 22:14:41 -04:00
}
2017-05-02 21:14:28 +02:00
2019-08-03 22:14:41 -04:00
// If the server shuts down, shut down the tray-icon as well
Action shutdownEvent = () =>
{
2025-07-01 09:41:31 -03:00
// Make sure we do not start again after
2019-08-03 22:14:41 -04:00
// a controlled exit
reSpawn = 100 ;
tk . InvokeExit ();
};
2025-07-02 20:42:35 +02:00
2025-03-25 10:56:01 +01:00
Connection . ConnectionClosed = shutdownEvent ;
2019-08-03 22:14:41 -04:00
if ( hosted != null )
2025-03-25 10:56:01 +01:00
hosted . InstanceShutdown = shutdownEvent ;
2025-07-02 20:42:35 +02:00
2019-08-03 22:14:41 -04:00
tk . Init ( _args );
2017-05-02 21:14:28 +02:00
2019-08-03 22:14:41 -04:00
// If the tray-icon quits, stop the server
reSpawn = 100 ;
2017-06-27 15:05:46 +02:00
2019-08-03 22:14:41 -04:00
// Make sure that the server shutdown does not access the tray-icon,
// as it would be disposed by now
if ( hosted != null )
2025-03-25 10:56:01 +01:00
hosted . InstanceShutdown = null ;
Connection . ConnectionClosed = null ;
2017-05-02 21:14:28 +02:00
}
}
2025-07-02 20:42:35 +02:00
2019-08-03 22:14:41 -04:00
}
2025-02-08 17:30:27 +01:00
catch ( HttpRequestException ex )
2019-08-03 22:14:41 -04:00
{
System . Diagnostics . Trace . WriteLine ( "Request error: " + ex );
Console . WriteLine ( "Request error: " + ex );
2017-05-03 00:03:16 +02:00
2019-08-03 22:14:41 -04:00
reSpawn ++;
}
catch ( Exception ex )
{
System . Diagnostics . Trace . WriteLine ( "Unexpected error: " + ex );
Console . WriteLine ( "Unexpected error: " + ex );
return ;
}
} while ( reSpawn < 3 );
2017-05-02 21:14:28 +02:00
}
}
2024-03-04 14:34:38 +01:00
private static TrayIconBase RunTrayIcon ()
=> new AvaloniaRunner ();
2011-11-28 11:05:42 +00:00
2025-03-25 11:13:57 +01:00
public static ICommandLineArgument [] DetachSupportedCommands =>
[
new CommandLineArgument(NOHOSTEDSERVER_OPTION, CommandLineArgument.ArgumentType.String, "Disables local server", "Set this option to not spawn a local service, use if the TrayIcon should connect to a running service"),
new CommandLineArgument(READCONFIGFROMDB_OPTION, CommandLineArgument.ArgumentType.String, "Read server connection info from DB", $"Set this option to read server connection info for running service from its database (only together with {NOHOSTEDSERVER_OPTION})"),
2025-03-25 11:36:44 +01:00
.. WebserverSupportedCommands.Where(x => DETATCHED_WEBERVER_OPTIONS.Contains(x.Name))
2025-03-25 11:13:57 +01:00
] ;
public static ICommandLineArgument [] BasicSupportedCommands =>
[
2025-03-25 11:36:44 +01:00
new CommandLineArgument(HOSTURL_OPTION, CommandLineArgument.ArgumentType.String, "Selects the url to connect to", "Supply the url that the TrayIcon will connect to and show status for", DEFAULT_HOSTURL),
2025-03-25 11:13:57 +01:00
new CommandLineArgument(BROWSER_COMMAND_OPTION, CommandLineArgument.ArgumentType.String, "Sets the browser command", "Set this option to override the default browser detection"),
new CommandLineArgument(ACCEPTED_SSL_CERTIFICATE, CommandLineArgument.ArgumentType.String, "Accepts a specific SSL certificate", "Set this option to accept a specific SSL certificate, the value should be the hash of the certificate in hexadecimal format. Use * to accept any certificate (dangerous)"),
.. WindowsSupportedCommands
] ;
private static ICommandLineArgument [] WindowsSupportedCommands =>
OperatingSystem . IsWindows ()
? [ new CommandLineArgument ( DETACHED_PROCESS , CommandLineArgument . ArgumentType . String , "Runs the tray-icon detached" , "This option runs the tray-icon in detached mode, meaning that the process will exit immediately and not send output to the console of the caller" )]
: [];
public static ICommandLineArgument [] WebserverSupportedCommands
=> Server . Program . SupportedCommands ;
public static ICommandLineArgument [] AllSupportedCommands =>
[
.. DetachSupportedCommands,
.. BasicSupportedCommands
] ;
2011-06-06 18:48:52 +00:00
}
}