This PR fixes a race during startup, where it was possible for the scheduler to start before the runner was paused, such that tasks could be started, even if the server was supposed to be paused. This also fixes a more direct issue where the state was not updated during startup, so the UI could look paused, but the runner was active and accepting tasks. This fixes #6634
1200 lines
69 KiB
C#
1200 lines
69 KiB
C#
// 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;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Duplicati.Library.AutoUpdater;
|
|
using Duplicati.Library.Common.IO;
|
|
using Duplicati.Library.Crashlog;
|
|
using Duplicati.Library.Encryption;
|
|
using Duplicati.Library.Interface;
|
|
using Duplicati.Library.Logging;
|
|
using Duplicati.Library.Main;
|
|
using Duplicati.Library.Main.Database;
|
|
using Duplicati.Library.Utility;
|
|
using Duplicati.Server.Database;
|
|
using Duplicati.WebserverCore;
|
|
using Duplicati.WebserverCore.Abstractions;
|
|
using Duplicati.WebserverCore.Services;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
namespace Duplicati.Server
|
|
{
|
|
public class Program
|
|
{
|
|
/// <summary>The commandline argument name for parameters file.</summary>
|
|
private const string PARAMETERS_FILE_OPTION = "parameters-file";
|
|
/// <summary>Alternative names for the parameters file option.</summary>
|
|
private static readonly string[] PARAMETERS_FILE_OPTION_EXTRAS = ["parameterfile"];
|
|
/// <summary>The commandline argument name for ping-pong keepalive.</summary>
|
|
private const string PING_PONG_KEEPALIVE_OPTION = "ping-pong-keepalive";
|
|
/// <summary>The commandline argument name for Windows event log.</summary>
|
|
private const string WINDOWS_EVENTLOG_OPTION = "windows-eventlog";
|
|
/// <summary>The commandline argument name for Windows event log level.</summary>
|
|
private const string WINDOWS_EVENTLOG_LEVEL_OPTION = "windows-eventlog-level";
|
|
/// <summary>The commandline argument name for disabling database encryption.</summary>
|
|
private const string DISABLE_DB_ENCRYPTION_OPTION = "disable-db-encryption";
|
|
/// <summary>The commandline argument name for requiring database encryption key.</summary>
|
|
private const string REQUIRE_DB_ENCRYPTION_KEY_OPTION = "require-db-encryption-key";
|
|
/// <summary>The commandline argument name for settings encryption key.</summary>
|
|
private const string SETTINGS_ENCRYPTION_KEY_OPTION = "settings-encryption-key";
|
|
/// <summary>The commandline argument name for disabling update check.</summary>
|
|
private const string DISABLE_UPDATE_CHECK_OPTION = "disable-update-check";
|
|
/// <summary>The commandline argument name for registering remote control.</summary>
|
|
private const string REGISTER_REMOTE_CONTROL_OPTION = "register-remote-control";
|
|
/// <summary>The commandline argument name for forcing remote control reregistration.</summary>
|
|
private const string REGISTER_REMOTE_CONTROL_REREGISTER_OPTION = "register-remote-control-force";
|
|
/// <summary>The commandline argument name for allowed backend modules.</summary>
|
|
private const string ALLOWED_BACKEND_MODULES = "allowed-backend-modules";
|
|
/// <summary>The commandline argument name for allowed encryption modules.</summary>
|
|
private const string ALLOWED_ENCRYPTION_MODULES = "allowed-encryption-modules";
|
|
/// <summary>The commandline argument name for allowed compression modules.</summary>
|
|
private const string ALLOWED_COMPRESSION_MODULES = "allowed-compression-modules";
|
|
/// <summary>The commandline argument name for log file.</summary>
|
|
private const string LOG_FILE_OPTION = "log-file";
|
|
/// <summary>The commandline argument name for log level.</summary>
|
|
private const string LOG_LEVEL_OPTION = "log-level";
|
|
/// <summary>The commandline argument name for log console.</summary>
|
|
private const string LOG_CONSOLE_OPTION = "log-console";
|
|
/// <summary>The commandline argument name for temp directory.</summary>
|
|
private const string TEMPDIR_OPTION = "tempdir";
|
|
/// <summary>The commandline argument name for log retention.</summary>
|
|
private const string LOG_RETENTION_OPTION = "log-retention";
|
|
/// <summary>The commandline argument name for help.</summary>
|
|
private const string HELP_OPTION = "help";
|
|
|
|
|
|
#if DEBUG
|
|
private const bool DEBUG_MODE = true;
|
|
#else
|
|
private const bool DEBUG_MODE = false;
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// Options to be ignored when validating the command line
|
|
/// </summary>
|
|
public static readonly HashSet<string> ValidationIgnoredOptions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
/// <summary>
|
|
/// The log tag for messages from this class
|
|
/// </summary>
|
|
private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType<Program>();
|
|
/// <summary>
|
|
/// The path to the directory that contains the main executable
|
|
/// </summary>
|
|
public static readonly string StartupPath = Duplicati.Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR;
|
|
|
|
/// <summary>
|
|
/// The environment variable prefix
|
|
/// </summary>
|
|
private static readonly string ENV_NAME_PREFIX = AutoUpdateSettings.AppName.ToUpperInvariant();
|
|
|
|
/// <summary>
|
|
/// The single instance
|
|
/// </summary>
|
|
public static SingleInstance ApplicationInstance = null;
|
|
|
|
/// <summary>
|
|
/// The thread running the ping-pong handler
|
|
/// </summary>
|
|
private static Thread PingPongThread;
|
|
|
|
/// <summary>
|
|
/// The controller interface for pause/resume and throttle options
|
|
/// </summary>
|
|
public static LiveControls LiveControl { get => DuplicatiWebserver.Provider.GetRequiredService<LiveControls>(); }
|
|
|
|
/// <summary>
|
|
/// Duplicati webserver instance
|
|
/// </summary>
|
|
public static DuplicatiWebserver DuplicatiWebserver { get; set; }
|
|
|
|
/// <summary>
|
|
/// Callback to shutdown the modern webserver
|
|
/// </summary>
|
|
private static void ShutdownModernWebserver()
|
|
{
|
|
DuplicatiWebserver.Stop().GetAwaiter().GetResult();
|
|
}
|
|
|
|
/// <summary>
|
|
/// An event that is set once the server is ready to respond to requests
|
|
/// </summary>
|
|
public static readonly ManualResetEvent ServerStartedEvent = new ManualResetEvent(false);
|
|
|
|
/// <summary>
|
|
/// Timer for purging temp files and log data
|
|
/// </summary>
|
|
private static System.Threading.Timer PurgeTempFilesTimer = null;
|
|
|
|
/// <summary>
|
|
/// The main entry point for the application.
|
|
/// </summary>
|
|
/// <param name="_args"> The command line arguments</param>
|
|
[STAThread]
|
|
public static int Main(string[] _args)
|
|
=> Main(null, _args);
|
|
|
|
/// <summary>
|
|
/// The main entry point for the application.
|
|
/// </summary>
|
|
/// <param name="applicationSettings"> The application settings</param>
|
|
/// <param name="_args"> The command line arguments</param>
|
|
[STAThread]
|
|
public static int Main(IApplicationSettings applicationSettings, string[] _args)
|
|
{
|
|
PreloadSettingsLoader.ConfigurePreloadSettings(ref _args, PackageHelper.NamedExecutable.Server, out var preloadDbSettings);
|
|
|
|
applicationSettings ??= new ApplicationSettings();
|
|
|
|
//If this executable is invoked directly, write to console, otherwise throw exceptions
|
|
var writeToConsoleOnException = applicationSettings.Origin == "Server";
|
|
|
|
// Prepared for the future, where we might want to have a silent console mode
|
|
var silentConsole = false;
|
|
var logMessageToConsole = (string message) => { if (!silentConsole) Console.WriteLine(message); };
|
|
|
|
//Find commandline options here for handling special startup cases
|
|
var args = new List<string>(_args);
|
|
var optionsWithFilter = FilterCollector.ExtractOptions(new List<string>(args));
|
|
var commandlineOptions = optionsWithFilter.Item1;
|
|
var filter = optionsWithFilter.Item2;
|
|
|
|
if (HelpOptionExtensions.IsArgumentAnyHelpString(args))
|
|
{
|
|
return ShowHelp(writeToConsoleOnException);
|
|
}
|
|
|
|
if (commandlineOptions.ContainsKey("tempdir") && !string.IsNullOrEmpty(commandlineOptions["tempdir"]))
|
|
{
|
|
SystemContextSettings.DefaultTempPath = commandlineOptions["tempdir"];
|
|
}
|
|
|
|
SystemContextSettings.StartSession();
|
|
|
|
ApplyEnvironmentVariables(commandlineOptions);
|
|
ApplySecretProvider(applicationSettings, commandlineOptions, CancellationToken.None).Await();
|
|
|
|
var parameterFileOption = PARAMETERS_FILE_OPTION_EXTRAS.Prepend(PARAMETERS_FILE_OPTION)
|
|
.FirstOrDefault(x => commandlineOptions.ContainsKey(x));
|
|
|
|
if (parameterFileOption != null && !string.IsNullOrEmpty(commandlineOptions[parameterFileOption]))
|
|
{
|
|
string filename = commandlineOptions[parameterFileOption];
|
|
commandlineOptions.Remove(parameterFileOption);
|
|
ReadOptionsFromFile(filename, ref filter, args, commandlineOptions);
|
|
}
|
|
|
|
var logHandler = new LogWriteHandler();
|
|
using var logScope = ConfigureLogging(logHandler, commandlineOptions);
|
|
|
|
// Validate after logging is configured
|
|
CommandLineArgumentValidator.ValidateArguments(SupportedCommands, commandlineOptions, KnownDuplicateOptions, ValidationIgnoredOptions);
|
|
|
|
// Unload any modules that are not allowed
|
|
Library.DynamicLoader.BackendLoader.OnlyAllowBackends(commandlineOptions.GetValueOrDefault(ALLOWED_BACKEND_MODULES));
|
|
Library.DynamicLoader.EncryptionLoader.OnlyAllowModules(commandlineOptions.GetValueOrDefault(ALLOWED_ENCRYPTION_MODULES));
|
|
Library.DynamicLoader.CompressionLoader.OnlyAllowModules(commandlineOptions.GetValueOrDefault(ALLOWED_COMPRESSION_MODULES));
|
|
|
|
|
|
var crashed = false;
|
|
var terminated = false;
|
|
IQueueRunnerService queueRunner = null;
|
|
UpdatePollThread updatePollThread = null;
|
|
EventPollNotify eventPollNotify = null;
|
|
ISchedulerService scheduler = null;
|
|
try
|
|
{
|
|
var connection = GetDatabaseConnection(applicationSettings, commandlineOptions, silentConsole, true);
|
|
|
|
if (!connection.ApplicationSettings.FixedInvalidBackupId)
|
|
connection.FixInvalidBackupId();
|
|
|
|
connection.ApplicationSettings.UpgradePasswordToKBDF();
|
|
CreateApplicationInstance(applicationSettings.DataFolder, writeToConsoleOnException);
|
|
|
|
applicationSettings.StartOrStopUsageReporter = () => StartOrStopUsageReporter(connection);
|
|
applicationSettings.StartOrStopUsageReporter?.Invoke();
|
|
|
|
AdjustApplicationSettings(connection, commandlineOptions);
|
|
|
|
UpdaterManager.OnError += obj =>
|
|
{
|
|
connection.LogError(null, "Error in updater", obj);
|
|
};
|
|
|
|
DuplicatiWebserver = StartWebServer(commandlineOptions, connection, logHandler, applicationSettings).Await();
|
|
|
|
connection.SetServiceProvider(DuplicatiWebserver.Provider);
|
|
queueRunner = DuplicatiWebserver.Provider.GetRequiredService<IQueueRunnerService>();
|
|
updatePollThread = DuplicatiWebserver.Provider.GetRequiredService<UpdatePollThread>();
|
|
eventPollNotify = DuplicatiWebserver.Provider.GetRequiredService<EventPollNotify>();
|
|
scheduler = DuplicatiWebserver.Provider.GetRequiredService<ISchedulerService>();
|
|
|
|
updatePollThread.Init(Library.Utility.Utility.ParseBoolOption(commandlineOptions, DISABLE_UPDATE_CHECK_OPTION));
|
|
|
|
SetPurgeTempFilesTimer(connection, commandlineOptions);
|
|
|
|
LiveControl.StateChanged = (e) => { LiveControl_StateChanged(queueRunner, connection, eventPollNotify, scheduler, e); };
|
|
|
|
// Invoke it once to set initial state
|
|
LiveControl_StateChanged(queueRunner, connection, eventPollNotify, scheduler, new LiveControls.LiveControlEvent
|
|
{
|
|
State = LiveControl.State,
|
|
TransfersPaused = LiveControl.TransfersPaused,
|
|
WaitTimeExpiration = LiveControl.EstimatedPauseEnd
|
|
});
|
|
|
|
if (Library.Utility.Utility.ParseBoolOption(commandlineOptions, PING_PONG_KEEPALIVE_OPTION))
|
|
{
|
|
PingPongThread = new Thread(() => PingPongMethod(applicationSettings)) { IsBackground = true };
|
|
PingPongThread.Start();
|
|
}
|
|
|
|
connection.ReWriteAllFieldsIfEncryptionChanged();
|
|
connection.SetPreloadSettingsIfChanged(preloadDbSettings);
|
|
EmitWarningsForConfigurationIssues(connection, applicationSettings, commandlineOptions);
|
|
|
|
Log.WriteInformationMessage(LOGTAG, "ServerStarted", Strings.Program.ServerStarted(DuplicatiWebserver.Interface, DuplicatiWebserver.Port));
|
|
logMessageToConsole(Strings.Program.ServerStarted(DuplicatiWebserver.Interface, DuplicatiWebserver.Port));
|
|
|
|
var remoteControlUrl = commandlineOptions.GetValueOrDefault(REGISTER_REMOTE_CONTROL_OPTION);
|
|
if (!string.IsNullOrWhiteSpace(remoteControlUrl))
|
|
RegisterForRemoteControl(DuplicatiWebserver.Provider.GetRequiredService<IRemoteControllerRegistration>(), DuplicatiWebserver.Provider.GetRequiredService<IRemoteController>(), remoteControlUrl, Library.Utility.Utility.ParseBoolOption(commandlineOptions, REGISTER_REMOTE_CONTROL_REREGISTER_OPTION), logMessageToConsole);
|
|
|
|
if (applicationSettings.Origin == "Server" && connection.ApplicationSettings.AutogeneratedPassphrase)
|
|
{
|
|
var signinToken = DuplicatiWebserver.Provider.GetRequiredService<IJWTTokenProvider>().CreateSigninToken("server-cli");
|
|
var hostname = (connection.ApplicationSettings.AllowedHostnames ?? string.Empty).Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(x => x != "*") ?? "localhost";
|
|
var scheme = connection.ApplicationSettings.UseHTTPS ? "https" : "http";
|
|
|
|
var url = $"{scheme}://{hostname}:{DuplicatiWebserver.Port}/signin.html?token={signinToken}";
|
|
Log.WriteWarningMessage(LOGTAG, "ServerStartedSignin", null, Strings.Program.ServerStartedSignin(url));
|
|
logMessageToConsole(Strings.Program.ServerStartedSignin(url));
|
|
}
|
|
|
|
DuplicatiWebserver.TerminationTask.ContinueWith((t) =>
|
|
{
|
|
if (t.Exception != null)
|
|
{
|
|
Log.WriteWarningMessage(LOGTAG, "ServerCrashed", t.Exception, Strings.Program.ServerCrashed(t.Exception.Message));
|
|
logMessageToConsole(Strings.Program.ServerStartedSignin(Strings.Program.ServerCrashed(t.Exception.ToString())));
|
|
}
|
|
|
|
terminated = true;
|
|
applicationSettings.SignalApplicationExit();
|
|
});
|
|
|
|
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();
|
|
applicationSettings.ApplicationExit.WaitHandle.WaitOne();
|
|
}
|
|
catch (SingleInstance.MultipleInstanceException mex)
|
|
{
|
|
crashed = true;
|
|
Log.WriteErrorMessage(LOGTAG, "MultipleInstanceError", mex, Strings.Program.ServerCrashed(mex.Message));
|
|
System.Diagnostics.Trace.WriteLine(Strings.Program.SeriousError(mex.ToString()));
|
|
if (!writeToConsoleOnException) throw;
|
|
|
|
Console.WriteLine(Strings.Program.SeriousError(mex.ToString()));
|
|
return 100;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
crashed = true;
|
|
Log.WriteErrorMessage(LOGTAG, "ServerCrashed", ex, Strings.Program.ServerCrashed(ex.Message));
|
|
System.Diagnostics.Trace.WriteLine(Strings.Program.SeriousError(ex.ToString()));
|
|
if (writeToConsoleOnException)
|
|
{
|
|
Console.WriteLine(Strings.Program.SeriousError(ex.ToString()));
|
|
return 100;
|
|
}
|
|
else
|
|
throw new Exception(Strings.Program.SeriousError(ex.ToString()), ex);
|
|
}
|
|
finally
|
|
{
|
|
Log.WriteInformationMessage(LOGTAG, "ServerStopping", Strings.Program.ServerStopping);
|
|
|
|
var steps = new Action[] {
|
|
() => eventPollNotify?.SignalNewEvent(),
|
|
() => ShutdownModernWebserver(),
|
|
() => updatePollThread?.Terminate(),
|
|
() => scheduler?.Terminate(true),
|
|
() => queueRunner?.Terminate(true),
|
|
() => ApplicationInstance?.Dispose(),
|
|
() => PurgeTempFilesTimer?.Dispose(),
|
|
() => Library.UsageReporter.Reporter.ShutDown(),
|
|
() => PingPongThread?.Interrupt(),
|
|
() =>
|
|
{
|
|
Log.WriteInformationMessage(LOGTAG, "ServerStopped", Strings.Program.ServerStopped);
|
|
logHandler?.Dispose();
|
|
}
|
|
};
|
|
|
|
foreach (var teardownStep in steps)
|
|
{
|
|
try
|
|
{
|
|
teardownStep();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// If the server is already crashed, that is the main error
|
|
// If the server crashes during teardown, we log that as an error
|
|
if (!(crashed || terminated))
|
|
{
|
|
System.Diagnostics.Trace.WriteLine(Strings.Program.TearDownError(ex.ToString()));
|
|
logMessageToConsole(Strings.Program.TearDownError(ex.ToString()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts the Duplicati web server
|
|
/// </summary>
|
|
/// <param name="options">The commandline options</param>
|
|
/// <param name="connection">The connection to use</param>
|
|
/// <param name="logWriteHandler">The log write handler</param>
|
|
/// <param name="applicationSettings">The application settings</param>
|
|
/// <returns></returns>
|
|
private static async Task<DuplicatiWebserver> StartWebServer(IReadOnlyDictionary<string, string> options, Connection connection, ILogWriteHandler logWriteHandler, IApplicationSettings applicationSettings)
|
|
{
|
|
var server = await WebServerLoader.TryRunServer(options, connection, async parsedOptions =>
|
|
{
|
|
var mappedSettings = new DuplicatiWebserver.InitSettings(
|
|
parsedOptions.WebRoot,
|
|
parsedOptions.Port,
|
|
parsedOptions.Interface,
|
|
parsedOptions.Certificate,
|
|
parsedOptions.AllowedHostnames,
|
|
parsedOptions.DisableStaticFiles,
|
|
parsedOptions.TokenLifetimeInMinutes,
|
|
parsedOptions.SPAPaths,
|
|
parsedOptions.CorsOrigins,
|
|
parsedOptions.PreAuthTokens
|
|
);
|
|
|
|
var server = DuplicatiWebserver.CreateWebServer(mappedSettings, connection, logWriteHandler, applicationSettings);
|
|
|
|
// Start the server, but catch any configuration issues
|
|
var task = server.Start();
|
|
await Task.WhenAny(task, Task.Delay(500));
|
|
if (task.IsCompleted)
|
|
await task;
|
|
|
|
return server;
|
|
}).ConfigureAwait(false);
|
|
|
|
connection.ApplicationSettings.ServerPortChanged |= server.Port != connection.ApplicationSettings.LastWebserverPort;
|
|
connection.ApplicationSettings.LastWebserverPort = server.Port;
|
|
|
|
return server;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets up the timer for purging temp files and log data
|
|
/// </summary>
|
|
/// <param name="connection">The connection to use</param>
|
|
/// <param name="commandlineOptions">The commandline options</param>
|
|
private static void SetPurgeTempFilesTimer(Connection connection, Dictionary<string, string> commandlineOptions)
|
|
{
|
|
var lastPurge = new DateTime(0);
|
|
|
|
TimerCallback purgeTempFilesCallback = (x) =>
|
|
{
|
|
try
|
|
{
|
|
if (Math.Abs((DateTime.Now - lastPurge).TotalHours) < (DEBUG_MODE ? 1 : 23))
|
|
{
|
|
return;
|
|
}
|
|
|
|
lastPurge = DateTime.Now;
|
|
|
|
foreach (var e in connection.GetTempFiles().Where((f) => f.Expires < DateTime.Now))
|
|
{
|
|
try
|
|
{
|
|
if (System.IO.File.Exists(e.Path))
|
|
System.IO.File.Delete(e.Path);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
connection.LogError(null, $"Failed to delete temp file: {e.Path}", ex);
|
|
}
|
|
|
|
connection.DeleteTempFile(e.ID);
|
|
}
|
|
|
|
|
|
Library.Utility.TempFile.RemoveOldApplicationTempFiles((path, ex) =>
|
|
{
|
|
connection.LogError(null, $"Failed to delete temp file: {path}", ex);
|
|
});
|
|
|
|
if (!commandlineOptions.TryGetValue("log-retention", out string pts))
|
|
{
|
|
pts = DEFAULT_LOG_RETENTION;
|
|
}
|
|
|
|
connection.PurgeLogData(Library.Utility.Timeparser.ParseTimeInterval(pts, DateTime.Now, true));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
connection.LogError(null, "Failed during temp file cleanup", ex);
|
|
}
|
|
};
|
|
|
|
PurgeTempFilesTimer =
|
|
new System.Threading.Timer(purgeTempFilesCallback, null,
|
|
DEBUG_MODE ? TimeSpan.FromSeconds(10) : TimeSpan.FromHours(1),
|
|
DEBUG_MODE ? TimeSpan.FromHours(1) : TimeSpan.FromDays(1));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers the server for remote control
|
|
/// </summary>
|
|
/// <param name="remoteControllerRegistration">The remote controller registration</param>
|
|
/// <param name="remoteController">The remote controller</param>
|
|
/// <param name="remoteControlUrl">The remote control URL</param>
|
|
/// <param name="reRegister">Whether to re-register</param>
|
|
/// <param name="logMessageToConsole">Callback to log messages to console</param>
|
|
private static void RegisterForRemoteControl(IRemoteControllerRegistration remoteControllerRegistration, IRemoteController remoteController, string remoteControlUrl, bool reRegister, Action<string> logMessageToConsole)
|
|
{
|
|
if (remoteController.CanEnable)
|
|
{
|
|
if (!reRegister)
|
|
{
|
|
Log.WriteInformationMessage(LOGTAG, "RemoteControlAlreadyRegistered", Strings.Program.RemoteControlAlreadyRegistered);
|
|
return;
|
|
}
|
|
|
|
remoteController.DeleteRegistration();
|
|
}
|
|
|
|
Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
var regTask = remoteControllerRegistration.RegisterMachine(remoteControlUrl);
|
|
while (!regTask.IsCompleted)
|
|
{
|
|
// Interface does not have events, so poll it every second
|
|
await Task.WhenAny(Task.Delay(1000), regTask).ConfigureAwait(false);
|
|
if (!string.IsNullOrWhiteSpace(remoteControllerRegistration.RegistrationUrl))
|
|
{
|
|
Log.WriteWarningMessage(LOGTAG, "RemoteControlRegistrationUrl", null, Strings.Program.RemoteControlRegistrationUrl(remoteControllerRegistration.RegistrationUrl));
|
|
logMessageToConsole(Strings.Program.RemoteControlRegistrationUrl(remoteControllerRegistration.RegistrationUrl));
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Await the registration task and ensure any errors are logged
|
|
await regTask.ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.WriteErrorMessage(LOGTAG, "RemoteControlRegistrationFailed", ex, Strings.Program.RemoteControlRegistrationFailed(ex.Message));
|
|
}
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adjusts the application settings based on commandline options
|
|
/// </summary>
|
|
/// <param name="connection">The connection to use</param>
|
|
/// <param name="commandlineOptions">The commandline options</param>
|
|
private static void AdjustApplicationSettings(Connection connection, Dictionary<string, string> commandlineOptions)
|
|
{
|
|
// This clears the JWT config, and a new will be generated, invalidating all existing tokens
|
|
if (Library.Utility.Utility.ParseBoolOption(commandlineOptions, WebServerLoader.OPTION_WEBSERVICE_RESET_JWT_CONFIG))
|
|
{
|
|
connection.ApplicationSettings.JWTConfig = null;
|
|
// Clean up stored tokens as they are now invalid
|
|
connection.ExecuteWithCommand((con) => con.ExecuteNonQuery("DELETE FROM TokenFamily"));
|
|
}
|
|
|
|
if (Library.Utility.Utility.ParseBoolOption(commandlineOptions, WebServerLoader.OPTION_WEBSERVICE_ENABLE_FOREVER_TOKEN))
|
|
connection.ApplicationSettings.EnableForeverTokens();
|
|
|
|
if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_DISABLE_SIGNIN_TOKENS))
|
|
connection.ApplicationSettings.DisableSigninTokens = Library.Utility.Utility.ParseBool(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_DISABLE_SIGNIN_TOKENS], true);
|
|
|
|
if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_DISABLEAPIEXTENSIONS))
|
|
connection.ApplicationSettings.DisabledAPIExtensions = commandlineOptions.GetValueOrDefault(WebServerLoader.OPTION_WEBSERVICE_DISABLEAPIEXTENSIONS)?
|
|
.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
.ToHashSet(StringComparer.OrdinalIgnoreCase) ?? [];
|
|
|
|
if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_PASSWORD))
|
|
connection.ApplicationSettings.SetWebserverPassword(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_PASSWORD]);
|
|
|
|
if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES))
|
|
connection.ApplicationSettings.SetAllowedHostnames(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES]);
|
|
else if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES_ALT))
|
|
connection.ApplicationSettings.SetAllowedHostnames(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES_ALT]);
|
|
|
|
if (commandlineOptions.ContainsKey(WebServerLoader.OPTION_WEBSERVICE_TIMEZONE) && !string.IsNullOrEmpty(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_TIMEZONE]))
|
|
try
|
|
{
|
|
connection.ApplicationSettings.Timezone = TimeZoneHelper.FindTimeZone(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_TIMEZONE]);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new UserInformationException(Strings.Program.InvalidTimezone(commandlineOptions[WebServerLoader.OPTION_WEBSERVICE_TIMEZONE]), "InvalidTimeZone", ex);
|
|
}
|
|
|
|
// The database has recorded a new version
|
|
if (connection.ApplicationSettings.UpdatedVersion != null)
|
|
{
|
|
// Check if the running version is newer than the recorded version
|
|
if (UpdaterManager.TryParseVersion(connection.ApplicationSettings.UpdatedVersion.Version) <= UpdaterManager.TryParseVersion(UpdaterManager.SelfVersion.Version))
|
|
{
|
|
// Clean up lingering update notifications
|
|
var updateNotifications = connection.GetNotifications().Where(x => x.Action == "update:new").ToList();
|
|
foreach (var n in updateNotifications)
|
|
connection.DismissNotification(n.ID);
|
|
|
|
// Clear up the recorded version
|
|
connection.ApplicationSettings.UpdatedVersion = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits warnings for configuration issues
|
|
/// </summary>
|
|
/// <param name="connection">The connection</param>
|
|
/// <param name="applicationSettings">The application settings</param>
|
|
/// <param name="commandlineOptions">The commandline options</param>
|
|
private static void EmitWarningsForConfigurationIssues(Connection connection, IApplicationSettings applicationSettings, Dictionary<string, string> commandlineOptions)
|
|
{
|
|
// First, remove any pending notifications, if the issue is resolved
|
|
if (connection.IsEncryptingFields)
|
|
{
|
|
// Remove any existing unencrypted database notification
|
|
var updateNotifications = connection.GetNotifications().Where(x => x.Action == "config:issue:unencrypted-database").ToList();
|
|
foreach (var n in updateNotifications)
|
|
connection.DismissNotification(n.ID);
|
|
}
|
|
|
|
if (OperatingSystem.IsWindows() && !applicationSettings.DataFolder.StartsWith(Util.AppendDirSeparator(Environment.GetFolderPath(Environment.SpecialFolder.Windows)), StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// Remove any existing windows folder used notification
|
|
var updateNotifications = connection.GetNotifications().Where(x => x.Action == "config:issue:windows-folder-used").ToList();
|
|
foreach (var n in updateNotifications)
|
|
connection.DismissNotification(n.ID);
|
|
}
|
|
|
|
// Emit warnings if the application has been updated
|
|
if (connection.ApplicationSettings.LastConfigIssueCheckVersion != UpdaterManager.SelfVersion.Version)
|
|
{
|
|
var updateNotifications = connection.GetNotifications().Where(x => x.Action.StartsWith("config:issue:")).ToList();
|
|
foreach (var n in updateNotifications)
|
|
connection.DismissNotification(n.ID);
|
|
|
|
if (!connection.IsEncryptingFields && !Library.Utility.Utility.ParseBoolOption(commandlineOptions, DISABLE_DB_ENCRYPTION_OPTION))
|
|
{
|
|
connection.RegisterNotification(
|
|
Serialization.NotificationType.Warning,
|
|
"Unencrypted database",
|
|
"The database is not encrypted. This is a security risk and should be fixed as soon as possible.",
|
|
null,
|
|
null,
|
|
"config:issue:unencrypted-database",
|
|
null,
|
|
"UnencryptedDatabase",
|
|
null,
|
|
(self, all) =>
|
|
{
|
|
return all.FirstOrDefault(x => x.Action == "config:issue:unencrypted-database") ?? self;
|
|
}
|
|
);
|
|
}
|
|
|
|
if (OperatingSystem.IsWindows() && applicationSettings.DataFolder.StartsWith(Util.AppendDirSeparator(Environment.GetFolderPath(Environment.SpecialFolder.Windows)), StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
connection.RegisterNotification(
|
|
Serialization.NotificationType.Warning,
|
|
"Incorrect storage folder",
|
|
"The server configuration is stored inside the Windows folder. Please move the configuration to a different location, or it may be deleted on Windows version upgrades.",
|
|
null,
|
|
null,
|
|
"config:issue:windows-folder-used",
|
|
null,
|
|
"UnencryptedDatabase",
|
|
null,
|
|
(self, all) =>
|
|
{
|
|
return all.FirstOrDefault(x => x.Action == "config:issue:windows-folder-used") ?? self;
|
|
}
|
|
);
|
|
}
|
|
|
|
connection.ApplicationSettings.LastConfigIssueCheckVersion = UpdaterManager.SelfVersion.Version;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the application instance to ensure a single instance is running for the current user
|
|
/// </summary>
|
|
/// <param name="dataFolder">The data folder to use for single instance locking</param>
|
|
/// <param name="writeToConsoleOnException">Write to console on exception</param>
|
|
private static void CreateApplicationInstance(string dataFolder, bool writeToConsoleOnException)
|
|
{
|
|
try
|
|
{
|
|
//This will also create DATAFOLDER if it does not exist
|
|
ApplicationInstance = new SingleInstance(dataFolder);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (writeToConsoleOnException)
|
|
{
|
|
Console.WriteLine(Strings.Program.StartupFailure(ex));
|
|
Environment.Exit(200);
|
|
}
|
|
|
|
throw new Exception(Strings.Program.StartupFailure(ex));
|
|
}
|
|
|
|
if (!ApplicationInstance.IsFirstInstance)
|
|
{
|
|
if (writeToConsoleOnException)
|
|
{
|
|
Console.WriteLine(Strings.Program.AnotherInstanceDetected);
|
|
Environment.Exit(200);
|
|
}
|
|
|
|
throw new SingleInstance.MultipleInstanceException(Strings.Program.AnotherInstanceDetected);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies environment variables by converting them to commandline options
|
|
/// </summary>
|
|
/// <param name="commandlineOptions">The commandline options</param>
|
|
private static void ApplyEnvironmentVariables(Dictionary<string, string> commandlineOptions)
|
|
{
|
|
foreach (var key in SupportedCommands.SelectMany(x => (x.Aliases ?? []).Prepend(x.Name)).Distinct())
|
|
{
|
|
// Commandline options take precedence
|
|
if (commandlineOptions.ContainsKey(key))
|
|
continue;
|
|
|
|
var envkey = $"{ENV_NAME_PREFIX}__{key.Replace('-', '_').ToUpperInvariant()}";
|
|
var envval = Environment.GetEnvironmentVariable(envkey);
|
|
if (!string.IsNullOrWhiteSpace(envval))
|
|
commandlineOptions[key] = envval;
|
|
}
|
|
|
|
// Set the encryption key from the environment variable
|
|
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME)) && string.IsNullOrWhiteSpace(commandlineOptions.GetValueOrDefault(SETTINGS_ENCRYPTION_KEY_OPTION)))
|
|
commandlineOptions[SETTINGS_ENCRYPTION_KEY_OPTION] = Environment.GetEnvironmentVariable(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies the secret provider to the commandline options and application settings
|
|
/// </summary>
|
|
/// <param name="applicationSettings">The application settings</param>
|
|
/// <param name="commandlineOptions">The commandline options</param>
|
|
/// <param name="cancellationToken">The cancellation token</param>
|
|
/// <returns>A task that represents the asynchronous operation</returns>
|
|
private static async Task ApplySecretProvider(IApplicationSettings applicationSettings, Dictionary<string, string> commandlineOptions, CancellationToken cancellationToken)
|
|
=> applicationSettings.SecretProvider = await SecretProviderHelper.ApplySecretProviderAsync([], [], commandlineOptions, TempFolder.SystemTempPath, applicationSettings.SecretProvider, cancellationToken).ConfigureAwait(false);
|
|
|
|
/// <summary>
|
|
/// A log destination that writes to the console
|
|
/// </summary>
|
|
/// <param name="level">The minimum log level</param>
|
|
private class ConsoleLogDestination(LogMessageType level) : ILogDestination
|
|
{
|
|
/// <inheritdoc />
|
|
public void WriteMessage(LogEntry entry)
|
|
{
|
|
if (entry.Level >= level)
|
|
Console.WriteLine(entry.AsString(true));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configures logging based on commandline options
|
|
/// </summary>
|
|
/// <param name="logWriteHandler">The log write handler</param>
|
|
/// <param name="commandlineOptions">The commandline options</param>
|
|
/// <returns>A disposable log scope</returns>
|
|
private static IDisposable ConfigureLogging(ILogWriteHandler logWriteHandler, Dictionary<string, string> commandlineOptions)
|
|
{
|
|
IDisposable logScope;
|
|
//Log various information in the logfile
|
|
if (DEBUG_MODE && !commandlineOptions.ContainsKey(LOG_FILE_OPTION))
|
|
{
|
|
var prefix = System.Reflection.Assembly.GetEntryAssembly().GetName().Name.StartsWith("Duplicati.Server") ? "server" : "trayicon";
|
|
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]);
|
|
}
|
|
|
|
logScope = Log.StartScope(logWriteHandler, null);
|
|
|
|
if (commandlineOptions.ContainsKey(LOG_FILE_OPTION))
|
|
{
|
|
var loglevel = Library.Logging.LogMessageType.Warning;
|
|
if (commandlineOptions.ContainsKey(LOG_LEVEL_OPTION))
|
|
Enum.TryParse(commandlineOptions[LOG_LEVEL_OPTION], true, out loglevel);
|
|
|
|
logWriteHandler.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);
|
|
|
|
logWriteHandler.AppendLogDestination(new ConsoleLogDestination(loglevel), loglevel);
|
|
}
|
|
|
|
if (commandlineOptions.TryGetValue(WINDOWS_EVENTLOG_OPTION, out var source) && !string.IsNullOrEmpty(source))
|
|
{
|
|
if (!OperatingSystem.IsWindows())
|
|
{
|
|
Log.WriteWarningMessage(LOGTAG, "WindowsLogNotSupported", null, Strings.Program.WindowsEventLogNotSupported);
|
|
}
|
|
else
|
|
{
|
|
if (!WindowsEventLogSource.SourceExists(source))
|
|
{
|
|
Log.WriteInformationMessage(LOGTAG, "WindowsLogMissingCreating", null, Strings.Program.WindowsEventLogSourceNotFound(source));
|
|
try
|
|
{
|
|
WindowsEventLogSource.CreateEventSource(source);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.WriteWarningMessage(LOGTAG, "WindowsLogFailedCreate", ex, Strings.Program.WindowsEventLogSourceNotCreated(source));
|
|
}
|
|
}
|
|
|
|
if (WindowsEventLogSource.SourceExists(source))
|
|
{
|
|
var loglevel = LogMessageType.Information;
|
|
if (commandlineOptions.ContainsKey(WINDOWS_EVENTLOG_LEVEL_OPTION))
|
|
Enum.TryParse(commandlineOptions[WINDOWS_EVENTLOG_LEVEL_OPTION], true, out loglevel);
|
|
|
|
logWriteHandler.AppendLogDestination(new WindowsEventLogSource(source), loglevel);
|
|
}
|
|
}
|
|
}
|
|
|
|
CrashlogHelper.OnUnobservedTaskException += (ex) => logWriteHandler.WriteMessage(new LogEntry(ex.Message, null, Library.Logging.LogMessageType.Error, LOGTAG, "UnobservedTaskException", ex));
|
|
return logScope;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shows the help information
|
|
/// </summary>
|
|
/// <param name="writeToConsoleOnException">Whether to write to console on exception</param>
|
|
/// <returns>An integer indicating the result</returns>
|
|
private static int ShowHelp(bool writeToConsoleOnException)
|
|
{
|
|
if (writeToConsoleOnException)
|
|
{
|
|
Console.WriteLine(Strings.Program.HelpDisplayDialog);
|
|
|
|
foreach (var arg in SupportedCommands)
|
|
Console.WriteLine(Strings.Program.HelpDisplayFormat(arg.Name, arg.LongDescription));
|
|
|
|
return 0;
|
|
}
|
|
|
|
throw new Exception("Server invoked with --help");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the database connection, upgrading the database if needed
|
|
/// </summary>
|
|
/// <param name="applicationSettings">The application settings</param>
|
|
/// <param name="commandlineOptions">The commandline options</param>
|
|
/// <param name="silentConsole">Whether to suppress console output</param>
|
|
/// <param name="changeDbEncryption">Whether to change the database encryption state if the arguments require it</param>
|
|
/// <returns>The database connection</returns>
|
|
public static Connection GetDatabaseConnection(IApplicationSettings applicationSettings, Dictionary<string, string> commandlineOptions, bool silentConsole, bool changeDbEncryption)
|
|
{
|
|
// Emit a warning if the database is stored in the Windows folder
|
|
if (Util.IsPathUnderWindowsFolder(applicationSettings.DataFolder))
|
|
Log.WriteWarningMessage(LOGTAG, "DatabaseInWindowsFolder", null, "The database is stored in the Windows folder, this is not recommended as it will be deleted on Windows upgrades.");
|
|
|
|
CrashlogHelper.DefaultLogDir = applicationSettings.DataFolder;
|
|
|
|
var sqliteVersion = new Version(Duplicati.Library.SQLiteHelper.SQLiteLoader.SQLiteVersion);
|
|
if (sqliteVersion < new Version(3, 6, 3))
|
|
{
|
|
//The official Mono SQLite provider is also broken with less than 3.6.3
|
|
throw new Exception(Strings.Program.WrongSQLiteVersion(sqliteVersion, "3.6.3"));
|
|
}
|
|
|
|
//Create the connection instance
|
|
var con = Library.SQLiteHelper.SQLiteLoader.LoadConnection();
|
|
|
|
try
|
|
{
|
|
var databasePath = System.IO.Path.Combine(applicationSettings.DataFolder, DataFolderManager.SERVER_DATABASE_FILENAME);
|
|
|
|
if (!System.IO.Directory.Exists(System.IO.Path.GetDirectoryName(databasePath)))
|
|
System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(databasePath));
|
|
|
|
// Attempt to open the database, removing any encryption present
|
|
Library.SQLiteHelper.SQLiteLoader.OpenDatabaseAsync(con, databasePath, Library.SQLiteHelper.SQLiteRC4Decrypter.GetEncryptionPassword(commandlineOptions)).Await();
|
|
|
|
Library.SQLiteHelper.DatabaseUpgrader.UpgradeDatabase(con, databasePath, typeof(Library.RestAPI.Database.DatabaseSchemaMarker));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
//Unwrap the reflection exceptions
|
|
if (ex is System.Reflection.TargetInvocationException && ex.InnerException != null)
|
|
ex = ex.InnerException;
|
|
|
|
throw new Exception(Strings.Program.DatabaseOpenError(ex.Message), ex);
|
|
}
|
|
|
|
var disableDbEncryption = Library.Utility.Utility.ParseBoolOption(commandlineOptions, DISABLE_DB_ENCRYPTION_OPTION);
|
|
var requireDbEncryptionKey = Library.Utility.Utility.ParseBoolOption(commandlineOptions, REQUIRE_DB_ENCRYPTION_KEY_OPTION);
|
|
var encKey = EncryptedFieldHelper.KeyInstance.CreateKeyIfValid(commandlineOptions.GetValueOrDefault(SETTINGS_ENCRYPTION_KEY_OPTION));
|
|
var usingBlacklistedKey = encKey?.IsBlacklisted ?? false;
|
|
var hasValidEncryptionKey = encKey != null;
|
|
|
|
applicationSettings.SettingsEncryptionKeyProvidedExternally = hasValidEncryptionKey;
|
|
|
|
if (requireDbEncryptionKey && !(hasValidEncryptionKey || disableDbEncryption))
|
|
throw new UserInformationException(Strings.Program.DatabaseEncryptionKeyRequired(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME, DISABLE_DB_ENCRYPTION_OPTION), "RequireDbEncryptionKey");
|
|
|
|
var hasEncryptedFields = false;
|
|
try
|
|
{
|
|
using (var cmd = con.CreateCommand(@$"SELECT ""Value"" FROM ""Option"" WHERE ""Name"" = @Name AND ""BackupID"" = @BackupId"))
|
|
hasEncryptedFields = Library.Utility.Utility.ParseBool(cmd
|
|
.SetParameterValue("@Name", Database.ServerSettings.CONST.ENCRYPTED_FIELDS)
|
|
.SetParameterValue("@BackupId", Connection.SERVER_SETTINGS_ID).ExecuteScalar()?.ToString(), false);
|
|
|
|
if (hasEncryptedFields && !hasValidEncryptionKey)
|
|
{
|
|
Log.WriteWarningMessage(LOGTAG, "EncryptionKeyMissing", null, Strings.Program.EncryptionKeyMissing(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME));
|
|
if (!silentConsole)
|
|
Console.WriteLine(Strings.Program.EncryptionKeyMissing(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME));
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// Ignore errors here, as we are just checking for a potential issue
|
|
// Only negative effect is that we do not show a potentially helpful warning
|
|
}
|
|
|
|
if (!hasValidEncryptionKey && !disableDbEncryption)
|
|
{
|
|
disableDbEncryption = true;
|
|
Log.WriteWarningMessage(LOGTAG, "MissingEncryptionKey", null, Strings.Program.NoEncryptionKeySpecified(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME, DISABLE_DB_ENCRYPTION_OPTION));
|
|
if (!silentConsole)
|
|
Console.WriteLine(Strings.Program.NoEncryptionKeySpecified(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME, DISABLE_DB_ENCRYPTION_OPTION));
|
|
}
|
|
|
|
if (usingBlacklistedKey && !disableDbEncryption)
|
|
{
|
|
disableDbEncryption = true;
|
|
Log.WriteErrorMessage(LOGTAG, "BlacklistedEncryptionKey", null, Strings.Program.BlacklistedEncryptionKey(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME, DISABLE_DB_ENCRYPTION_OPTION));
|
|
if (!silentConsole)
|
|
Console.WriteLine(Strings.Program.BlacklistedEncryptionKey(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME, DISABLE_DB_ENCRYPTION_OPTION));
|
|
}
|
|
|
|
// If the database is not encrypted, and we are not changing the encryption
|
|
// don't pass the key, as that would cause the database to be encrypted
|
|
if (!hasEncryptedFields && !changeDbEncryption && hasValidEncryptionKey)
|
|
encKey = null;
|
|
|
|
return new Connection(con, disableDbEncryption, encKey, applicationSettings.DataFolder, applicationSettings.StartOrStopUsageReporter);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts or stops the usage reporter based on application settings
|
|
/// </summary>
|
|
/// <param name="connection">The connection</param>
|
|
private static void StartOrStopUsageReporter(Connection connection)
|
|
{
|
|
var disableUsageReporter =
|
|
string.Equals(connection.ApplicationSettings.UsageReporterLevel, "none", StringComparison.OrdinalIgnoreCase)
|
|
||
|
|
string.Equals(connection.ApplicationSettings.UsageReporterLevel, "disabled", StringComparison.OrdinalIgnoreCase);
|
|
|
|
if (!Enum.TryParse<Library.UsageReporter.ReportType>(connection.ApplicationSettings.UsageReporterLevel, true, out var reportLevel))
|
|
Library.UsageReporter.Reporter.SetReportLevel(null, disableUsageReporter);
|
|
else
|
|
Library.UsageReporter.Reporter.SetReportLevel(reportLevel, disableUsageReporter);
|
|
}
|
|
|
|
/// <summary>
|
|
/// This event handler updates the trayicon menu with the current state of the runner.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
|
private static void LiveControl_StateChanged(IQueueRunnerService queueRunnerService, Connection connection, EventPollNotify eventPollNotify, ISchedulerService schedulerService, LiveControls.LiveControlEvent e)
|
|
{
|
|
var appSettings = connection.ApplicationSettings;
|
|
switch (e.State)
|
|
{
|
|
case LiveControls.LiveControlState.Paused:
|
|
{
|
|
queueRunnerService.Pause();
|
|
queueRunnerService.GetCurrentTask()?.Pause(e.TransfersPaused);
|
|
appSettings.PausedUntil = e.WaitTimeExpiration;
|
|
break;
|
|
}
|
|
case LiveControls.LiveControlState.Running:
|
|
{
|
|
queueRunnerService.Resume();
|
|
queueRunnerService.GetCurrentTask()?.Resume();
|
|
schedulerService?.Reschedule();
|
|
appSettings.PausedUntil = null;
|
|
break;
|
|
}
|
|
default:
|
|
Log.WriteWarningMessage(LOGTAG, "InvalidPauseResumeState", null, Strings.Program.InvalidPauseResumeState(LiveControl.State));
|
|
break;
|
|
}
|
|
|
|
eventPollNotify.SignalNewEvent();
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Simple method for tracking if the server has crashed
|
|
/// </summary>
|
|
/// <param name="applicationSettings">The application settings</param>
|
|
private static void PingPongMethod(IApplicationSettings applicationSettings)
|
|
{
|
|
var rd = new System.IO.StreamReader(Console.OpenStandardInput());
|
|
var wr = new System.IO.StreamWriter(Console.OpenStandardOutput());
|
|
string line;
|
|
while ((line = rd.ReadLine()) != null)
|
|
{
|
|
if (string.Equals("shutdown", line, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// TODO: All calls to ApplicationExitEvent and TrayIcon->Quit
|
|
// should check if we are running something
|
|
applicationSettings.SignalApplicationExit();
|
|
}
|
|
else
|
|
{
|
|
wr.WriteLine("pong");
|
|
wr.Flush();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The default log retention
|
|
/// </summary>
|
|
private static readonly string DEFAULT_LOG_RETENTION = "30D";
|
|
|
|
/// <summary>
|
|
/// The options related to the secret provider
|
|
/// </summary>
|
|
private static readonly IReadOnlyList<ICommandLineArgument> SECRET_PROVIDER_OPTIONS = new Options(new Dictionary<string, string>()).SupportedCommands.Where(x => x.Name.StartsWith("secret-provider")).ToList();
|
|
|
|
/// <summary>
|
|
/// Gets additional commandline arguments support on Windows
|
|
/// </summary>
|
|
private static readonly ICommandLineArgument[] WindowsOptions = OperatingSystem.IsWindows()
|
|
? [
|
|
new CommandLineArgument(WINDOWS_EVENTLOG_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.Program.LogwindowseventlogShort, Strings.Program.LogwindowseventlogLong),
|
|
new CommandLineArgument(WINDOWS_EVENTLOG_LEVEL_OPTION, CommandLineArgument.ArgumentType.Enumeration, Strings.Program.LogwindowseventloglevelShort, Strings.Program.LogwindowseventloglevelLong, Library.Logging.LogMessageType.Information.ToString(), null, Enum.GetNames(typeof(Duplicati.Library.Logging.LogMessageType)))
|
|
]
|
|
: [];
|
|
|
|
/// <summary>
|
|
/// Gets a list of all supported commandline options
|
|
/// </summary>
|
|
public static ICommandLineArgument[] SupportedCommands =>
|
|
[
|
|
.. WindowsOptions,
|
|
new CommandLineArgument(TEMPDIR_OPTION, CommandLineArgument.ArgumentType.Path, Strings.Program.TempdirShort, Strings.Program.TempdirLong, System.IO.Path.GetTempPath()),
|
|
new CommandLineArgument(HELP_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.Program.HelpCommandDescription, Strings.Program.HelpCommandDescription),
|
|
new CommandLineArgument(PARAMETERS_FILE_OPTION, CommandLineArgument.ArgumentType.Path, Strings.Program.ParametersFileOptionShort, Strings.Program.ParametersFileOptionLong, "", PARAMETERS_FILE_OPTION_EXTRAS),
|
|
new CommandLineArgument(DataFolderManager.PORTABLE_MODE_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.Program.PortablemodeCommandDescription, Strings.Program.PortablemodeCommandDescription, DataFolderManager.PORTABLE_MODE.ToString().ToLowerInvariant()),
|
|
new CommandLineArgument(LOG_FILE_OPTION, CommandLineArgument.ArgumentType.Path, Strings.Program.LogfileCommandDescription, Strings.Program.LogfileCommandDescription),
|
|
new CommandLineArgument(LOG_LEVEL_OPTION, CommandLineArgument.ArgumentType.Enumeration, Strings.Program.LoglevelCommandDescription, Strings.Program.LoglevelCommandDescription, Library.Logging.LogMessageType.Warning.ToString(), null, Enum.GetNames(typeof(Duplicati.Library.Logging.LogMessageType))),
|
|
new CommandLineArgument(LOG_CONSOLE_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.Program.LogConsoleDescription, Strings.Program.LogConsoleDescription, false.ToString()),
|
|
new CommandLineArgument(WebServerLoader.OPTION_WEBROOT, CommandLineArgument.ArgumentType.Path, Strings.Program.WebserverWebrootDescription, Strings.Program.WebserverWebrootDescription, WebServerLoader.DEFAULT_OPTION_WEBROOT),
|
|
new CommandLineArgument(WebServerLoader.OPTION_PORT, CommandLineArgument.ArgumentType.String, Strings.Program.WebserverPortDescription, Strings.Program.WebserverPortDescription, WebServerLoader.DEFAULT_OPTION_PORT.ToString()),
|
|
new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_DISABLEHTTPS, CommandLineArgument.ArgumentType.String, Strings.Program.WebserverDisableHTTPSDescription, Strings.Program.WebserverDisableHTTPSDescription),
|
|
new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_REMOVESSLCERTIFICATE, CommandLineArgument.ArgumentType.String, Strings.Program.WebserverRemoveCertificateDescription, Strings.Program.WebserverRemoveCertificateDescription),
|
|
new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_SSLCERTIFICATEFILE, CommandLineArgument.ArgumentType.String, Strings.Program.WebserverCertificateFileDescription, Strings.Program.WebserverCertificateFileDescription),
|
|
new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_SSLCERTIFICATEFILEPASSWORD, CommandLineArgument.ArgumentType.String, Strings.Program.WebserverCertificatePasswordDescription, Strings.Program.WebserverCertificatePasswordDescription),
|
|
new CommandLineArgument(WebServerLoader.OPTION_INTERFACE, CommandLineArgument.ArgumentType.String, Strings.Program.WebserverInterfaceDescription, Strings.Program.WebserverInterfaceDescription, WebServerLoader.DEFAULT_OPTION_INTERFACE),
|
|
new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_PASSWORD, CommandLineArgument.ArgumentType.Password, Strings.Program.WebserverPasswordDescription, Strings.Program.WebserverPasswordDescription),
|
|
new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES, CommandLineArgument.ArgumentType.String, Strings.Program.WebserverAllowedhostnamesDescription, Strings.Program.WebserverAllowedhostnamesDescription, null, [WebServerLoader.OPTION_WEBSERVICE_ALLOWEDHOSTNAMES_ALT]),
|
|
new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_RESET_JWT_CONFIG, CommandLineArgument.ArgumentType.Boolean, Strings.Program.WebserverResetJwtConfigDescription, Strings.Program.WebserverResetJwtConfigDescription),
|
|
new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_ENABLE_FOREVER_TOKEN, CommandLineArgument.ArgumentType.Boolean, Strings.Program.WebserverEnableForeverTokenDescription, Strings.Program.WebserverEnableForeverTokenDescription),
|
|
new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_DISABLEAPIEXTENSIONS, CommandLineArgument.ArgumentType.String, Strings.Program.WebserverDisableApiExtensionsDescription, Strings.Program.WebserverDisableApiExtensionsDescription),
|
|
new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_API_ONLY, CommandLineArgument.ArgumentType.Boolean, Strings.Program.WebserverApiOnlyDescription, Strings.Program.WebserverApiOnlyDescription),
|
|
new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_DISABLE_SIGNIN_TOKENS, CommandLineArgument.ArgumentType.Boolean, Strings.Program.WebserverDisableSigninTokensDescription, Strings.Program.WebserverDisableSigninTokensDescription),
|
|
new CommandLineArgument(WebServerLoader.OPTION_WEBSERVICE_SPAPATHS, CommandLineArgument.ArgumentType.Path, Strings.Program.WebserverSpaPathsDescription, Strings.Program.WebserverSpaPathsDescription, WebServerLoader.DEFAULT_OPTION_SPAPATHS),
|
|
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),
|
|
new CommandLineArgument(DataFolderManager.SERVER_DATAFOLDER_OPTION, CommandLineArgument.ArgumentType.Path, Strings.Program.ServerdatafolderShort, Strings.Program.ServerdatafolderLong(DataFolderManager.DATAFOLDER_ENV_NAME), DataFolderManager.GetDataFolder(DataFolderManager.AccessMode.ProbeOnly)),
|
|
new CommandLineArgument(DISABLE_DB_ENCRYPTION_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.Program.DisabledbencryptionShort, Strings.Program.DisabledbencryptionLong),
|
|
new CommandLineArgument(REQUIRE_DB_ENCRYPTION_KEY_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.Program.RequiredbencryptionShort, Strings.Program.RequiredbencryptionLong),
|
|
new CommandLineArgument(SETTINGS_ENCRYPTION_KEY_OPTION, CommandLineArgument.ArgumentType.Password, Strings.Program.SettingsencryptionkeyShort, Strings.Program.SettingsencryptionkeyLong(EncryptedFieldHelper.ENVIROMENT_VARIABLE_NAME)),
|
|
new CommandLineArgument(REGISTER_REMOTE_CONTROL_OPTION, CommandLineArgument.ArgumentType.String, Strings.Program.RegisterRemoteControlShort, Strings.Program.RegisterRemoteControlLong),
|
|
new CommandLineArgument(REGISTER_REMOTE_CONTROL_REREGISTER_OPTION, CommandLineArgument.ArgumentType.String, Strings.Program.RegisterRemoteControlReregisterShort, Strings.Program.RegisterRemoteControlReregisterLong),
|
|
new CommandLineArgument(ALLOWED_BACKEND_MODULES, CommandLineArgument.ArgumentType.String, Strings.Program.AllowedBackendModulesShort, Strings.Program.AllowedBackendModulesLong),
|
|
new CommandLineArgument(ALLOWED_ENCRYPTION_MODULES, CommandLineArgument.ArgumentType.String, Strings.Program.AllowedEncryptionModulesShort, Strings.Program.AllowedEncryptionModulesLong),
|
|
new CommandLineArgument(ALLOWED_COMPRESSION_MODULES, CommandLineArgument.ArgumentType.String, Strings.Program.AllowedCompressionModulesShort, Strings.Program.AllowedCompressionModulesLong),
|
|
.. SECRET_PROVIDER_OPTIONS
|
|
];
|
|
|
|
/// <summary>
|
|
/// List of known duplicate option names
|
|
/// </summary>
|
|
public static readonly HashSet<string> KnownDuplicateOptions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
/// <summary>
|
|
/// Reads options from a file and applies them to the current commandline arguments and options
|
|
/// </summary>
|
|
/// <param name="filename">The filename</param>
|
|
/// <param name="filter">The filter</param>
|
|
/// <param name="cargs">The commandline arguments</param>
|
|
/// <param name="options">The options</param>
|
|
/// <returns>True if successful</returns>
|
|
private static void ReadOptionsFromFile(string filename, ref IFilter filter, List<string> cargs, Dictionary<string, string> options)
|
|
{
|
|
try
|
|
{
|
|
var fargs = new List<string>(Library.Utility.Utility.ReadFileWithDefaultEncoding(Environment.ExpandEnvironmentVariables(filename)).Replace("\r\n", "\n").Replace("\r", "\n").Split(new String[] { "\n" }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()));
|
|
var newsource = new List<string>();
|
|
string newtarget = null;
|
|
string prependfilter = null;
|
|
string appendfilter = null;
|
|
string replacefilter = null;
|
|
|
|
var tmpparsed = FilterCollector.ExtractOptions(fargs, (key, value) =>
|
|
{
|
|
if (key.Equals("source", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
newsource.Add(value);
|
|
return false;
|
|
}
|
|
else if (key.Equals("target", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
newtarget = value;
|
|
return false;
|
|
}
|
|
else if (key.Equals("append-filter", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
appendfilter = value;
|
|
return false;
|
|
}
|
|
else if (key.Equals("prepend-filter", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
prependfilter = value;
|
|
return false;
|
|
}
|
|
else if (key.Equals("replace-filter", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
replacefilter = value;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
var opt = tmpparsed.Item1;
|
|
var newfilter = tmpparsed.Item2;
|
|
|
|
// If the user specifies parameters-file, all filters must be in the file.
|
|
// Allowing to specify some filters on the command line could result in wrong filter ordering
|
|
if (!filter.Empty && !newfilter.Empty)
|
|
throw new UserInformationException(Strings.Program.FiltersCannotBeUsedWithFileError2, "FiltersCannotBeUsedOnCommandLineAndInParameterFile");
|
|
|
|
if (!newfilter.Empty)
|
|
filter = newfilter;
|
|
|
|
if (!string.IsNullOrWhiteSpace(prependfilter))
|
|
filter = FilterExpression.Combine(FilterExpression.Deserialize(prependfilter.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries)), filter);
|
|
|
|
if (!string.IsNullOrWhiteSpace(appendfilter))
|
|
filter = FilterExpression.Combine(filter, FilterExpression.Deserialize(appendfilter.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries)));
|
|
|
|
if (!string.IsNullOrWhiteSpace(replacefilter))
|
|
filter = FilterExpression.Deserialize(replacefilter.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries));
|
|
|
|
foreach (var keyvalue in opt)
|
|
options[keyvalue.Key] = keyvalue.Value;
|
|
|
|
if (!string.IsNullOrEmpty(newtarget))
|
|
{
|
|
if (cargs.Count <= 1)
|
|
cargs.Add(newtarget);
|
|
else
|
|
cargs[1] = newtarget;
|
|
}
|
|
|
|
if (cargs.Count >= 1 && cargs[0].Equals("backup", StringComparison.OrdinalIgnoreCase))
|
|
cargs.AddRange(newsource);
|
|
else if (newsource.Count > 0)
|
|
Log.WriteVerboseMessage(LOGTAG, "NotUsingBackupSources", Strings.Program.SkippingSourceArgumentsOnNonBackupOperation);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
throw new Exception(Strings.Program.FailedToParseParametersFileError(filename, e.Message));
|
|
}
|
|
}
|
|
}
|
|
}
|