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") {