Fixed running server in-process from the WindowsService. This fixes the MSI not cleanly restarting a running service.

This commit is contained in:
Kenneth Skovhede
2026-06-01 16:45:00 +02:00
parent 36716949d0
commit b264e52a7a
7 changed files with 111 additions and 64 deletions
+19 -4
View File
@@ -90,6 +90,15 @@ namespace Duplicati.Server
/// <summary>The commandline argument name for help.</summary>
private const string HELP_OPTION = "help";
/// <summary>
/// The exit code for init password success
/// </summary>
public const int EXITCODE_INITPASSWORD_SUCCESS = 102;
/// <summary>
/// The exit code for init password failure
/// </summary>
public const int EXITCODE_INITPASSWORD_FAILED = 103;
/// <summary>
/// The name used in the secret provider for the database encryption key
/// </summary>
@@ -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
/// </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)
/// <returns>The exit code, or null if no exit is required</returns>
private static int? 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))
@@ -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;
}
/// <summary>
@@ -9,6 +9,7 @@
<ItemGroup>
<ProjectReference Include="..\Library\Utility\Duplicati.Library.Utility.csproj" />
<ProjectReference Include="..\Server\Duplicati.Server.csproj" />
<ProjectReference Include="..\Agent\Duplicati.Agent.csproj" />
</ItemGroup>
</Project>
+20 -20
View File
@@ -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;
+51 -10
View File
@@ -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<string, bool> logMessage = null, Action<System.Diagnostics.ProcessStartInfo> onBeforeFirstStart = null)
private readonly bool m_inProcess;
public Runner(
PackageHelper.NamedExecutable executable,
string[] cmdargs,
bool inProcess,
Action onStartedAction = null,
Action onStoppedAction = null,
Action<string, bool> logMessage = null,
Action<System.Diagnostics.ProcessStartInfo> 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)
@@ -13,6 +13,8 @@
<ItemGroup>
<ProjectReference Include="..\Library\AutoUpdater\Duplicati.Library.AutoUpdater.csproj" />
<ProjectReference Include="..\Library\Utility\Duplicati.Library.Utility.csproj" />
<ProjectReference Include="..\Server\Duplicati.Server.csproj" />
<ProjectReference Include="..\Agent\Duplicati.Agent.csproj" />
</ItemGroup>
<ItemGroup>
+17 -29
View File
@@ -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
@@ -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") {