diff --git a/Duplicati/Server/Program.cs b/Duplicati/Server/Program.cs index c5a8c09bf..d58e4ccf8 100644 --- a/Duplicati/Server/Program.cs +++ b/Duplicati/Server/Program.cs @@ -90,6 +90,15 @@ namespace Duplicati.Server /// The commandline argument name for help. private const string HELP_OPTION = "help"; + /// + /// The exit code for init password success + /// + public const int EXITCODE_INITPASSWORD_SUCCESS = 102; + /// + /// The exit code for init password failure + /// + public const int EXITCODE_INITPASSWORD_FAILED = 103; + /// /// The name used in the secret provider for the database encryption key /// @@ -263,7 +272,9 @@ namespace Duplicati.Server connection.UpdateUsageReporterCallback(applicationSettings.StartOrStopUsageReporter); applicationSettings.StartOrStopUsageReporter?.Invoke(); - AdjustApplicationSettings(connection, commandlineOptions); + var adjustres = AdjustApplicationSettings(connection, commandlineOptions); + if (adjustres.HasValue) + return adjustres.Value; UpdaterManager.OnError += obj => { @@ -622,7 +633,8 @@ namespace Duplicati.Server /// /// The connection to use /// The commandline options - private static void AdjustApplicationSettings(Connection connection, Dictionary commandlineOptions) + /// The exit code, or null if no exit is required + private static int? AdjustApplicationSettings(Connection connection, Dictionary 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)) @@ -652,14 +664,15 @@ namespace Duplicati.Server var initPassword = commandlineOptions.GetValueOrDefault(WebServerLoader.OPTION_WEBSERVICE_PASSWORD_INIT); if (!string.IsNullOrWhiteSpace(initPassword)) { + // If initPassword is set, we want the initialization to stop now, so we throw an exception if (connection.ApplicationSettings.AutogeneratedPassphrase) { connection.ApplicationSettings.SetWebserverPassword(initPassword); - Environment.Exit(0); + return EXITCODE_INITPASSWORD_SUCCESS; } else { - Environment.Exit(2); + return EXITCODE_INITPASSWORD_FAILED; } } @@ -693,6 +706,8 @@ namespace Duplicati.Server connection.ApplicationSettings.UpdatedVersion = null; } } + + return null; } /// diff --git a/Duplicati/Service/Duplicati.Service.csproj b/Duplicati/Service/Duplicati.Service.csproj index 58eb9d18c..0558c45fc 100644 --- a/Duplicati/Service/Duplicati.Service.csproj +++ b/Duplicati/Service/Duplicati.Service.csproj @@ -9,6 +9,7 @@ + diff --git a/Duplicati/Service/Program.cs b/Duplicati/Service/Program.cs index ee4faf89b..f982de4eb 100644 --- a/Duplicati/Service/Program.cs +++ b/Duplicati/Service/Program.cs @@ -1,22 +1,22 @@ -// Copyright (C) 2026, 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) 2026, 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; @@ -31,7 +31,7 @@ namespace Duplicati.Service { PreloadSettingsLoader.ConfigurePreloadSettings(ref args, PackageHelper.NamedExecutable.Service); - using (var runner = new Runner(PackageHelper.NamedExecutable.Server, args)) + using (var runner = new Runner(PackageHelper.NamedExecutable.Server, args, inProcess: false)) runner.Wait(); return 0; diff --git a/Duplicati/Service/Runner.cs b/Duplicati/Service/Runner.cs index 1d2b3c325..e0a6eb89c 100644 --- a/Duplicati/Service/Runner.cs +++ b/Duplicati/Service/Runner.cs @@ -43,8 +43,19 @@ namespace Duplicati.Service private readonly int WAIT_POLL_TIME = (int)TimeSpan.FromMinutes(15).TotalMilliseconds; - public Runner(PackageHelper.NamedExecutable executable, string[] cmdargs, Action onStartedAction = null, Action onStoppedAction = null, Action logMessage = null, Action onBeforeFirstStart = null) + private readonly bool m_inProcess; + + public Runner( + PackageHelper.NamedExecutable executable, + string[] cmdargs, + bool inProcess, + Action onStartedAction = null, + Action onStoppedAction = null, + Action logMessage = null, + Action onBeforeFirstStart = null + ) { + m_inProcess = inProcess; m_onStartedAction = onStartedAction; m_onStoppedAction = onStoppedAction; m_reportMessage = logMessage; @@ -54,13 +65,46 @@ namespace Duplicati.Service m_reportMessage = (x, y) => Console.WriteLine(x); m_cmdargs = cmdargs; - m_thread = new System.Threading.Thread(Run); + m_thread = new System.Threading.Thread(m_inProcess ? RunInProcess : RunSpawned); m_thread.IsBackground = true; m_thread.Name = "Server Runner"; m_thread.Start(); } - private void Run() + private void RunInProcess() + { + try + { + m_reportMessage(string.Format("Starting in-process {0} with cmd args {1}", m_executable, string.Join(Environment.NewLine, m_cmdargs ?? new string[0])), false); + + m_onBeforeFirstStart?.Invoke(null); + m_onStartedAction?.Invoke(); + + if (!m_terminate && !m_softstop) + { + if (m_executable == PackageHelper.NamedExecutable.Server) + Server.Program.Main(m_cmdargs ?? []); + else if (m_executable == PackageHelper.NamedExecutable.Agent) + Agent.Program.MainAsync(m_cmdargs ?? []).ConfigureAwait(false).GetAwaiter().GetResult(); + else + throw new NotImplementedException($"In-process execution of {m_executable} is not implemented"); + } + + if (!m_terminate && !m_softstop) + m_reportMessage("In-process execution ended unexpectedly", true); + } + catch (Exception ex) + { + m_reportMessage(string.Format("In-process execution has failed with error message: {0}", ex), true); + throw; + } + finally + { + m_onStoppedAction?.Invoke(); + } + } + + private void RunSpawned() { var path = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); var exec = System.IO.Path.Combine(path, PackageHelper.GetExecutableName(m_executable)); @@ -184,18 +228,15 @@ namespace Duplicati.Service m_thread.Join(); } - public void Stop(bool force = true) + public void Stop() { - if (force) + m_softstop = true; + if (m_inProcess) { - m_terminate = true; - var p = m_process; - if (p != null) - p.Kill(); + } else { - m_softstop = true; lock (m_writelock) { if (m_process != null) diff --git a/Duplicati/WindowsService/Duplicati.WindowsService.csproj b/Duplicati/WindowsService/Duplicati.WindowsService.csproj index 467fe976f..6d991a478 100644 --- a/Duplicati/WindowsService/Duplicati.WindowsService.csproj +++ b/Duplicati/WindowsService/Duplicati.WindowsService.csproj @@ -13,6 +13,8 @@ + + diff --git a/Duplicati/WindowsService/ServiceControl.cs b/Duplicati/WindowsService/ServiceControl.cs index fce0b2238..74fc3c1cc 100644 --- a/Duplicati/WindowsService/ServiceControl.cs +++ b/Duplicati/WindowsService/ServiceControl.cs @@ -175,6 +175,7 @@ namespace Duplicati.WindowsService m_runner = new Runner( m_executable, startargs, + true, () => { if (m_verbose_messages) @@ -206,7 +207,7 @@ namespace Duplicati.WindowsService (startInfo) => { // If we have a reset password, pass it to the server once - if (!string.IsNullOrEmpty(resetPassword)) + if (!string.IsNullOrEmpty(resetPassword) && startInfo != null) { startInfo.EnvironmentVariables["DUPLICATI__WEBSERVICE_PASSWORD"] = resetPassword; resetPassword = null; @@ -234,7 +235,7 @@ namespace Duplicati.WindowsService if (m_verbose_messages) m_eventLog.WriteEntry("Soft stop invoked..."); - m_runner.Stop(false); + m_runner.Stop(); } } @@ -402,35 +403,22 @@ namespace Duplicati.WindowsService { m_eventLog.WriteEntry("Found init password in registry, applying...", System.Diagnostics.EventLogEntryType.Information); - var installDir = UpdaterManager.INSTALLATIONDIR; - var exeName = PackageHelper.GetExecutableName(PackageHelper.NamedExecutable.Server); - var serverExe = System.IO.Path.Combine(installDir, exeName); + if (m_verbose_messages) + m_eventLog.WriteEntry($"Starting server in-process to apply init password...", System.Diagnostics.EventLogEntryType.Information); + + try + { + var res = Server.Program.Main(["--webservice-password-init=" + password]); + if (res == Server.Program.EXITCODE_INITPASSWORD_SUCCESS) + success = true; + } + catch (Exception ex) + { + m_eventLog.WriteEntry($"Exception from applying init password: {ex.Message}", System.Diagnostics.EventLogEntryType.Warning); + } if (m_verbose_messages) - m_eventLog.WriteEntry($"Starting server at {serverExe} to apply init password...", System.Diagnostics.EventLogEntryType.Information); - - if (System.IO.File.Exists(serverExe)) - { - var startInfo = new System.Diagnostics.ProcessStartInfo - { - FileName = serverExe, - UseShellExecute = false, - CreateNoWindow = true - }; - - startInfo.EnvironmentVariables["DUPLICATI__WEBSERVICE_PASSWORD_INIT"] = password; - - using (var process = System.Diagnostics.Process.Start(startInfo)) - { - if (process.WaitForExit(30000) && process.ExitCode == 0) - success = true; - - if (m_verbose_messages) - m_eventLog.WriteEntry($"Result from applying init password: {success}, code={(process.HasExited ? process.ExitCode.ToString() : "-alive-")}", System.Diagnostics.EventLogEntryType.Information); - if (!process.HasExited) - try { process.Kill(); } catch { } - } - } + m_eventLog.WriteEntry($"Result from applying init password: {success}", System.Diagnostics.EventLogEntryType.Information); } // Always delete the InitPassword key if found diff --git a/ReleaseBuilder/Resources/Windows/TrayIcon/CheckBootstrapResult.js b/ReleaseBuilder/Resources/Windows/TrayIcon/CheckBootstrapResult.js index ff025864d..913a49eba 100644 --- a/ReleaseBuilder/Resources/Windows/TrayIcon/CheckBootstrapResult.js +++ b/ReleaseBuilder/Resources/Windows/TrayIcon/CheckBootstrapResult.js @@ -60,7 +60,7 @@ function IsBootstrapApplied() { inParams.sValueName = "BootstrapApplied"; var outParams = reg.provider.ExecMethod_("GetStringValue", inParams, 0, reg.ctx); - if (outParams.ReturnValue !== 0) { + if (outParams.ReturnValue !== 0 || outParams.sValue === "unset") { return null; } if (outParams.sValue === "0") {