From 36917e9e8e658c466485fd2c69b4e1b0b4541f0a Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Mon, 19 Oct 2026 20:10:23 -0700 Subject: [PATCH 01/12] display the message if the SC fiales to start --- src/Particular.PlatformSample/AppLauncher.cs | 34 +++++++++++++++++-- .../PlatformLauncher.cs | 22 ++++++++++-- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/Particular.PlatformSample/AppLauncher.cs b/src/Particular.PlatformSample/AppLauncher.cs index 8d96453..bf53fd7 100644 --- a/src/Particular.PlatformSample/AppLauncher.cs +++ b/src/Particular.PlatformSample/AppLauncher.cs @@ -15,6 +15,25 @@ sealed class AppLauncher(bool showPlatformToolConsoleOutput) : IAsyncDisposable readonly string platformPath = Path.Combine(AppContext.BaseDirectory, "platform"); readonly bool hideConsoleOutput = !showPlatformToolConsoleOutput; readonly Stack> cleanupActions = new(); + readonly StringBuilder serviceControlErrorOutput = new(); + readonly List processes = []; + + public string ServiceControlErrorOutput => serviceControlErrorOutput.ToString(); + + public void StopProcesses() + { + foreach (var process in processes) + { + try + { + process.Kill(entireProcessTree: true); + } + catch (InvalidOperationException) + { + // The process has already exited + } + } + } public async Task RavenDB(string logsPath, string dataDirectory, CancellationToken cancellationToken = default) { @@ -81,7 +100,7 @@ await Console.Error.WriteLineAsync($"Failed to kill RavenDB server PID {processI } } - public void ServiceControl(int port, int maintenancePort, string logPath, string transportPath, int auditPort, Uri connectionString) + public Process ServiceControl(int port, int maintenancePort, string logPath, string transportPath, int auditPort, Uri connectionString) { var config = GetResource("Particular.configs.ServiceControl.exe.config"); @@ -101,7 +120,7 @@ public void ServiceControl(int port, int maintenancePort, string logPath, string return ValueTask.CompletedTask; }); - StartProcess(Path.Combine(platformPath, "servicecontrol", "servicecontrol-instance", "ServiceControl.dll")); + return StartProcess(Path.Combine(platformPath, "servicecontrol", "servicecontrol-instance", "ServiceControl.dll"), errorOutput: serviceControlErrorOutput); } public void ServiceControlAudit(int port, int maintenancePort, string logPath, string transportPath, Uri connectionString) @@ -163,7 +182,7 @@ public void ServicePulse(int port, int serviceControlPort, int monitoringPort, s StartProcess(Path.Combine(platformPath, "servicepulse", "ServicePulse.dll"), environmentVariables); } - void StartProcess(string assemblyPath, Dictionary environmentVariables = null) + Process StartProcess(string assemblyPath, Dictionary environmentVariables = null, StringBuilder errorOutput = null) { var workingDirectory = Path.GetDirectoryName(assemblyPath)!; var startInfo = new ProcessStartInfo("dotnet") @@ -194,9 +213,18 @@ void StartProcess(string assemblyPath, Dictionary environmentVar // without this in certain occasions the start takes waaaaay longer! if (hideConsoleOutput) { + // Keep the hidden error output so it can be shown if the process fails to start + if (errorOutput is not null) + { + process.ErrorDataReceived += (_, args) => errorOutput.AppendLine(args.Data); + } + process.BeginErrorReadLine(); process.BeginOutputReadLine(); } + + processes.Add(process); + return process; } static string GetResource(string resourceName) diff --git a/src/Particular.PlatformSample/PlatformLauncher.cs b/src/Particular.PlatformSample/PlatformLauncher.cs index dfa2d30..0bcfb8e 100644 --- a/src/Particular.PlatformSample/PlatformLauncher.cs +++ b/src/Particular.PlatformSample/PlatformLauncher.cs @@ -85,7 +85,7 @@ public static async Task Launch(bool showPlatformToolConsoleOutput = false, stri var serverUri = await launcher.RavenDB(ravenLogs, ravenDB, cancellationToken).ConfigureAwait(false); Console.WriteLine("Launching ServiceControl"); - launcher.ServiceControl(controlPort, maintenancePort, controlLogs, transportPath, auditPort, serverUri); + var serviceControl = launcher.ServiceControl(controlPort, maintenancePort, controlLogs, transportPath, auditPort, serverUri); Console.WriteLine("Launching ServiceControl Audit"); launcher.ServiceControlAudit(auditPort, auditMaintenancePort, auditLogs, transportPath, serverUri); @@ -97,8 +97,24 @@ public static async Task Launch(bool showPlatformToolConsoleOutput = false, stri launcher.ServicePulse(pulsePort, controlPort, monitoringPort, servicePulseDefaultRoute); Console.Write("Waiting for ServiceControl to be available"); - await Network.WaitForHttpOk($"http://localhost:{controlPort}/api", cancellationToken: tokenSource.Token) - .ConfigureAwait(false); + using var waitTokenSource = CancellationTokenSource.CreateLinkedTokenSource(tokenSource.Token); + var serviceControlAvailable = Network.WaitForHttpOk($"http://localhost:{controlPort}/api", cancellationToken: waitTokenSource.Token); + var serviceControlExited = serviceControl.WaitForExitAsync(waitTokenSource.Token); + + await Task.WhenAny(serviceControlAvailable, serviceControlExited).ConfigureAwait(false); + await waitTokenSource.CancelAsync().ConfigureAwait(false); + + // ServiceControl stops straight away if it can't start, for example when the .NET runtime version it requires is not installed + if (serviceControlExited.IsCompletedSuccessfully && !tokenSource.IsCancellationRequested) + { + Console.WriteLine(); + Console.WriteLine($"ServiceControl failed to start):"); + Console.WriteLine(launcher.ServiceControlErrorOutput); + launcher.StopProcesses(); + return; + } + + await serviceControlAvailable.ConfigureAwait(false); if (!tokenSource.IsCancellationRequested) { From 6fb083b71be11781c2627899941d4c075c484062 Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Mon, 19 Oct 2026 20:10:23 -0700 Subject: [PATCH 02/12] display the message if the SC fails to start --- src/Particular.PlatformSample/AppLauncher.cs | 34 +++++++++++++++++-- .../PlatformLauncher.cs | 22 ++++++++++-- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/Particular.PlatformSample/AppLauncher.cs b/src/Particular.PlatformSample/AppLauncher.cs index 8d96453..bf53fd7 100644 --- a/src/Particular.PlatformSample/AppLauncher.cs +++ b/src/Particular.PlatformSample/AppLauncher.cs @@ -15,6 +15,25 @@ sealed class AppLauncher(bool showPlatformToolConsoleOutput) : IAsyncDisposable readonly string platformPath = Path.Combine(AppContext.BaseDirectory, "platform"); readonly bool hideConsoleOutput = !showPlatformToolConsoleOutput; readonly Stack> cleanupActions = new(); + readonly StringBuilder serviceControlErrorOutput = new(); + readonly List processes = []; + + public string ServiceControlErrorOutput => serviceControlErrorOutput.ToString(); + + public void StopProcesses() + { + foreach (var process in processes) + { + try + { + process.Kill(entireProcessTree: true); + } + catch (InvalidOperationException) + { + // The process has already exited + } + } + } public async Task RavenDB(string logsPath, string dataDirectory, CancellationToken cancellationToken = default) { @@ -81,7 +100,7 @@ await Console.Error.WriteLineAsync($"Failed to kill RavenDB server PID {processI } } - public void ServiceControl(int port, int maintenancePort, string logPath, string transportPath, int auditPort, Uri connectionString) + public Process ServiceControl(int port, int maintenancePort, string logPath, string transportPath, int auditPort, Uri connectionString) { var config = GetResource("Particular.configs.ServiceControl.exe.config"); @@ -101,7 +120,7 @@ public void ServiceControl(int port, int maintenancePort, string logPath, string return ValueTask.CompletedTask; }); - StartProcess(Path.Combine(platformPath, "servicecontrol", "servicecontrol-instance", "ServiceControl.dll")); + return StartProcess(Path.Combine(platformPath, "servicecontrol", "servicecontrol-instance", "ServiceControl.dll"), errorOutput: serviceControlErrorOutput); } public void ServiceControlAudit(int port, int maintenancePort, string logPath, string transportPath, Uri connectionString) @@ -163,7 +182,7 @@ public void ServicePulse(int port, int serviceControlPort, int monitoringPort, s StartProcess(Path.Combine(platformPath, "servicepulse", "ServicePulse.dll"), environmentVariables); } - void StartProcess(string assemblyPath, Dictionary environmentVariables = null) + Process StartProcess(string assemblyPath, Dictionary environmentVariables = null, StringBuilder errorOutput = null) { var workingDirectory = Path.GetDirectoryName(assemblyPath)!; var startInfo = new ProcessStartInfo("dotnet") @@ -194,9 +213,18 @@ void StartProcess(string assemblyPath, Dictionary environmentVar // without this in certain occasions the start takes waaaaay longer! if (hideConsoleOutput) { + // Keep the hidden error output so it can be shown if the process fails to start + if (errorOutput is not null) + { + process.ErrorDataReceived += (_, args) => errorOutput.AppendLine(args.Data); + } + process.BeginErrorReadLine(); process.BeginOutputReadLine(); } + + processes.Add(process); + return process; } static string GetResource(string resourceName) diff --git a/src/Particular.PlatformSample/PlatformLauncher.cs b/src/Particular.PlatformSample/PlatformLauncher.cs index dfa2d30..0bcfb8e 100644 --- a/src/Particular.PlatformSample/PlatformLauncher.cs +++ b/src/Particular.PlatformSample/PlatformLauncher.cs @@ -85,7 +85,7 @@ public static async Task Launch(bool showPlatformToolConsoleOutput = false, stri var serverUri = await launcher.RavenDB(ravenLogs, ravenDB, cancellationToken).ConfigureAwait(false); Console.WriteLine("Launching ServiceControl"); - launcher.ServiceControl(controlPort, maintenancePort, controlLogs, transportPath, auditPort, serverUri); + var serviceControl = launcher.ServiceControl(controlPort, maintenancePort, controlLogs, transportPath, auditPort, serverUri); Console.WriteLine("Launching ServiceControl Audit"); launcher.ServiceControlAudit(auditPort, auditMaintenancePort, auditLogs, transportPath, serverUri); @@ -97,8 +97,24 @@ public static async Task Launch(bool showPlatformToolConsoleOutput = false, stri launcher.ServicePulse(pulsePort, controlPort, monitoringPort, servicePulseDefaultRoute); Console.Write("Waiting for ServiceControl to be available"); - await Network.WaitForHttpOk($"http://localhost:{controlPort}/api", cancellationToken: tokenSource.Token) - .ConfigureAwait(false); + using var waitTokenSource = CancellationTokenSource.CreateLinkedTokenSource(tokenSource.Token); + var serviceControlAvailable = Network.WaitForHttpOk($"http://localhost:{controlPort}/api", cancellationToken: waitTokenSource.Token); + var serviceControlExited = serviceControl.WaitForExitAsync(waitTokenSource.Token); + + await Task.WhenAny(serviceControlAvailable, serviceControlExited).ConfigureAwait(false); + await waitTokenSource.CancelAsync().ConfigureAwait(false); + + // ServiceControl stops straight away if it can't start, for example when the .NET runtime version it requires is not installed + if (serviceControlExited.IsCompletedSuccessfully && !tokenSource.IsCancellationRequested) + { + Console.WriteLine(); + Console.WriteLine($"ServiceControl failed to start):"); + Console.WriteLine(launcher.ServiceControlErrorOutput); + launcher.StopProcesses(); + return; + } + + await serviceControlAvailable.ConfigureAwait(false); if (!tokenSource.IsCancellationRequested) { From 489f65630aea8bff76a0ada1028684bc40743a05 Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Wed, 21 Oct 2026 21:10:59 -0700 Subject: [PATCH 03/12] fix typo --- src/Particular.PlatformSample/PlatformLauncher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Particular.PlatformSample/PlatformLauncher.cs b/src/Particular.PlatformSample/PlatformLauncher.cs index 0bcfb8e..1026fe5 100644 --- a/src/Particular.PlatformSample/PlatformLauncher.cs +++ b/src/Particular.PlatformSample/PlatformLauncher.cs @@ -108,7 +108,7 @@ public static async Task Launch(bool showPlatformToolConsoleOutput = false, stri if (serviceControlExited.IsCompletedSuccessfully && !tokenSource.IsCancellationRequested) { Console.WriteLine(); - Console.WriteLine($"ServiceControl failed to start):"); + Console.WriteLine("ServiceControl failed to start:"); Console.WriteLine(launcher.ServiceControlErrorOutput); launcher.StopProcesses(); return; From 2e4e9444fddbfb2446f51d8ccedd2165e7ce18bf Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Wed, 21 Oct 2026 21:14:06 -0700 Subject: [PATCH 04/12] Revert "fix typo" This reverts commit 489f65630aea8bff76a0ada1028684bc40743a05. --- src/Particular.PlatformSample/PlatformLauncher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Particular.PlatformSample/PlatformLauncher.cs b/src/Particular.PlatformSample/PlatformLauncher.cs index 1026fe5..0bcfb8e 100644 --- a/src/Particular.PlatformSample/PlatformLauncher.cs +++ b/src/Particular.PlatformSample/PlatformLauncher.cs @@ -108,7 +108,7 @@ public static async Task Launch(bool showPlatformToolConsoleOutput = false, stri if (serviceControlExited.IsCompletedSuccessfully && !tokenSource.IsCancellationRequested) { Console.WriteLine(); - Console.WriteLine("ServiceControl failed to start:"); + Console.WriteLine($"ServiceControl failed to start):"); Console.WriteLine(launcher.ServiceControlErrorOutput); launcher.StopProcesses(); return; From 7fd455a119991d78062f1d1ea905c83c13ba38c4 Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Wed, 21 Oct 2026 21:16:23 -0700 Subject: [PATCH 05/12] fix typo --- src/Particular.PlatformSample/PlatformLauncher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Particular.PlatformSample/PlatformLauncher.cs b/src/Particular.PlatformSample/PlatformLauncher.cs index 0bcfb8e..1026fe5 100644 --- a/src/Particular.PlatformSample/PlatformLauncher.cs +++ b/src/Particular.PlatformSample/PlatformLauncher.cs @@ -108,7 +108,7 @@ public static async Task Launch(bool showPlatformToolConsoleOutput = false, stri if (serviceControlExited.IsCompletedSuccessfully && !tokenSource.IsCancellationRequested) { Console.WriteLine(); - Console.WriteLine($"ServiceControl failed to start):"); + Console.WriteLine("ServiceControl failed to start:"); Console.WriteLine(launcher.ServiceControlErrorOutput); launcher.StopProcesses(); return; From f026b2b952532824776f47adff4ead80d24af38c Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Wed, 21 Oct 2026 21:54:16 -0700 Subject: [PATCH 06/12] update comment --- src/Particular.PlatformSample/PlatformLauncher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Particular.PlatformSample/PlatformLauncher.cs b/src/Particular.PlatformSample/PlatformLauncher.cs index 1026fe5..b514678 100644 --- a/src/Particular.PlatformSample/PlatformLauncher.cs +++ b/src/Particular.PlatformSample/PlatformLauncher.cs @@ -104,7 +104,7 @@ public static async Task Launch(bool showPlatformToolConsoleOutput = false, stri await Task.WhenAny(serviceControlAvailable, serviceControlExited).ConfigureAwait(false); await waitTokenSource.CancelAsync().ConfigureAwait(false); - // ServiceControl stops straight away if it can't start, for example when the .NET runtime version it requires is not installed + // ServiceControl exited on its own (not because of Ctrl+C), for example because the .NET runtime version it requires is not installed if (serviceControlExited.IsCompletedSuccessfully && !tokenSource.IsCancellationRequested) { Console.WriteLine(); From 6c2a8a460ccb2255927ae3b16849d81d27c2666a Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Wed, 21 Oct 2026 21:55:46 -0700 Subject: [PATCH 07/12] Ignore the null line sent when ServiceControl's error stream closes --- src/Particular.PlatformSample/AppLauncher.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Particular.PlatformSample/AppLauncher.cs b/src/Particular.PlatformSample/AppLauncher.cs index bf53fd7..fe9f642 100644 --- a/src/Particular.PlatformSample/AppLauncher.cs +++ b/src/Particular.PlatformSample/AppLauncher.cs @@ -216,7 +216,13 @@ Process StartProcess(string assemblyPath, Dictionary environment // Keep the hidden error output so it can be shown if the process fails to start if (errorOutput is not null) { - process.ErrorDataReceived += (_, args) => errorOutput.AppendLine(args.Data); + process.ErrorDataReceived += (_, args) => + { + if (args.Data is not null) + { + errorOutput.AppendLine(args.Data); + } + }; } process.BeginErrorReadLine(); From 00fa72a9ddded49fd8112091e1eb8d76e123093a Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Fri, 23 Oct 2026 17:15:07 -0700 Subject: [PATCH 08/12] display the error output from other instances as well --- src/Particular.PlatformSample/AppLauncher.cs | 42 +++++++++---------- src/Particular.PlatformSample/Network.cs | 4 +- .../PlatformLauncher.cs | 28 ++++++------- 3 files changed, 36 insertions(+), 38 deletions(-) diff --git a/src/Particular.PlatformSample/AppLauncher.cs b/src/Particular.PlatformSample/AppLauncher.cs index fe9f642..0346054 100644 --- a/src/Particular.PlatformSample/AppLauncher.cs +++ b/src/Particular.PlatformSample/AppLauncher.cs @@ -15,18 +15,17 @@ sealed class AppLauncher(bool showPlatformToolConsoleOutput) : IAsyncDisposable readonly string platformPath = Path.Combine(AppContext.BaseDirectory, "platform"); readonly bool hideConsoleOutput = !showPlatformToolConsoleOutput; readonly Stack> cleanupActions = new(); - readonly StringBuilder serviceControlErrorOutput = new(); - readonly List processes = []; + readonly List platformTools = []; - public string ServiceControlErrorOutput => serviceControlErrorOutput.ToString(); + public sealed record PlatformTool(string Name, Process Process, StringBuilder ErrorOutput); public void StopProcesses() { - foreach (var process in processes) + foreach (var tool in platformTools) { try { - process.Kill(entireProcessTree: true); + tool.Process.Kill(entireProcessTree: true); } catch (InvalidOperationException) { @@ -35,6 +34,8 @@ public void StopProcesses() } } + public List FindExitedTools() => platformTools.FindAll(tool => tool.Process.HasExited); + public async Task RavenDB(string logsPath, string dataDirectory, CancellationToken cancellationToken = default) { var licenseFilePath = Path.Combine(platformPath, "servicecontrol", "servicecontrol-instance", "Persisters", "RavenDB", "RavenLicense.json"); @@ -100,7 +101,7 @@ await Console.Error.WriteLineAsync($"Failed to kill RavenDB server PID {processI } } - public Process ServiceControl(int port, int maintenancePort, string logPath, string transportPath, int auditPort, Uri connectionString) + public void ServiceControl(int port, int maintenancePort, string logPath, string transportPath, int auditPort, Uri connectionString) { var config = GetResource("Particular.configs.ServiceControl.exe.config"); @@ -120,7 +121,7 @@ public Process ServiceControl(int port, int maintenancePort, string logPath, str return ValueTask.CompletedTask; }); - return StartProcess(Path.Combine(platformPath, "servicecontrol", "servicecontrol-instance", "ServiceControl.dll"), errorOutput: serviceControlErrorOutput); + StartProcess("ServiceControl", Path.Combine(platformPath, "servicecontrol", "servicecontrol-instance", "ServiceControl.dll")); } public void ServiceControlAudit(int port, int maintenancePort, string logPath, string transportPath, Uri connectionString) @@ -142,7 +143,7 @@ public void ServiceControlAudit(int port, int maintenancePort, string logPath, s return ValueTask.CompletedTask; }); - StartProcess(Path.Combine(platformPath, "servicecontrol", "servicecontrol-audit-instance", "ServiceControl.Audit.dll")); + StartProcess("ServiceControl Audit", Path.Combine(platformPath, "servicecontrol", "servicecontrol-audit-instance", "ServiceControl.Audit.dll")); } public void Monitoring(int port, string logPath, string transportPath) @@ -162,7 +163,7 @@ public void Monitoring(int port, string logPath, string transportPath) return ValueTask.CompletedTask; }); - StartProcess(Path.Combine(platformPath, "servicecontrol", "monitoring-instance", "ServiceControl.Monitoring.dll")); + StartProcess("ServiceControl Monitoring", Path.Combine(platformPath, "servicecontrol", "monitoring-instance", "ServiceControl.Monitoring.dll")); } public void ServicePulse(int port, int serviceControlPort, int monitoringPort, string defaultRoute) @@ -179,10 +180,10 @@ public void ServicePulse(int port, int serviceControlPort, int monitoringPort, s environmentVariables.Add("DEFAULT_ROUTE", defaultRoute); } - StartProcess(Path.Combine(platformPath, "servicepulse", "ServicePulse.dll"), environmentVariables); + StartProcess("ServicePulse", Path.Combine(platformPath, "servicepulse", "ServicePulse.dll"), environmentVariables); } - Process StartProcess(string assemblyPath, Dictionary environmentVariables = null, StringBuilder errorOutput = null) + void StartProcess(string name, string assemblyPath, Dictionary environmentVariables = null) { var workingDirectory = Path.GetDirectoryName(assemblyPath)!; var startInfo = new ProcessStartInfo("dotnet") @@ -209,28 +210,25 @@ Process StartProcess(string assemblyPath, Dictionary environment } var process = Process.Start(startInfo); + var tool = new PlatformTool(name, process, new StringBuilder()); // without this in certain occasions the start takes waaaaay longer! if (hideConsoleOutput) { - // Keep the hidden error output so it can be shown if the process fails to start - if (errorOutput is not null) + // Keep the hidden error output so it can be shown if the tool fails to start + process.ErrorDataReceived += (_, args) => { - process.ErrorDataReceived += (_, args) => + if (args.Data is not null) { - if (args.Data is not null) - { - errorOutput.AppendLine(args.Data); - } - }; - } + tool.ErrorOutput.AppendLine(args.Data); + } + }; process.BeginErrorReadLine(); process.BeginOutputReadLine(); } - processes.Add(process); - return process; + platformTools.Add(tool); } static string GetResource(string resourceName) diff --git a/src/Particular.PlatformSample/Network.cs b/src/Particular.PlatformSample/Network.cs index 2a15dff..f02f6f1 100644 --- a/src/Particular.PlatformSample/Network.cs +++ b/src/Particular.PlatformSample/Network.cs @@ -53,12 +53,12 @@ static bool TestPort(int port) } } - public static async Task WaitForHttpOk(string url, int timeoutMilliseconds = 1000, CancellationToken cancellationToken = default) + public static async Task WaitForHttpOk(string url, int timeoutMilliseconds = 1000, Func stopWaiting = null, CancellationToken cancellationToken = default) { var status = HttpStatusCode.Ambiguous; var c = new HttpClient(); - while (!cancellationToken.IsCancellationRequested && status != HttpStatusCode.OK) + while (!cancellationToken.IsCancellationRequested && status != HttpStatusCode.OK && stopWaiting?.Invoke() != true) { try { diff --git a/src/Particular.PlatformSample/PlatformLauncher.cs b/src/Particular.PlatformSample/PlatformLauncher.cs index b514678..d06f8a4 100644 --- a/src/Particular.PlatformSample/PlatformLauncher.cs +++ b/src/Particular.PlatformSample/PlatformLauncher.cs @@ -85,7 +85,7 @@ public static async Task Launch(bool showPlatformToolConsoleOutput = false, stri var serverUri = await launcher.RavenDB(ravenLogs, ravenDB, cancellationToken).ConfigureAwait(false); Console.WriteLine("Launching ServiceControl"); - var serviceControl = launcher.ServiceControl(controlPort, maintenancePort, controlLogs, transportPath, auditPort, serverUri); + launcher.ServiceControl(controlPort, maintenancePort, controlLogs, transportPath, auditPort, serverUri); Console.WriteLine("Launching ServiceControl Audit"); launcher.ServiceControlAudit(auditPort, auditMaintenancePort, auditLogs, transportPath, serverUri); @@ -97,25 +97,25 @@ public static async Task Launch(bool showPlatformToolConsoleOutput = false, stri launcher.ServicePulse(pulsePort, controlPort, monitoringPort, servicePulseDefaultRoute); Console.Write("Waiting for ServiceControl to be available"); - using var waitTokenSource = CancellationTokenSource.CreateLinkedTokenSource(tokenSource.Token); - var serviceControlAvailable = Network.WaitForHttpOk($"http://localhost:{controlPort}/api", cancellationToken: waitTokenSource.Token); - var serviceControlExited = serviceControl.WaitForExitAsync(waitTokenSource.Token); + await Network.WaitForHttpOk($"http://localhost:{controlPort}/api", stopWaiting: () => launcher.FindExitedTools().Count > 0, cancellationToken: tokenSource.Token) + .ConfigureAwait(false); - await Task.WhenAny(serviceControlAvailable, serviceControlExited).ConfigureAwait(false); - await waitTokenSource.CancelAsync().ConfigureAwait(false); - - // ServiceControl exited on its own (not because of Ctrl+C), for example because the .NET runtime version it requires is not installed - if (serviceControlExited.IsCompletedSuccessfully && !tokenSource.IsCancellationRequested) + // A platform tool exited on its own (not because of Ctrl+C), for example because the .NET runtime version it requires is not installed + var failedTools = launcher.FindExitedTools(); + if (failedTools.Count > 0 && !tokenSource.IsCancellationRequested) { - Console.WriteLine(); - Console.WriteLine("ServiceControl failed to start:"); - Console.WriteLine(launcher.ServiceControlErrorOutput); + foreach (var failedTool in failedTools) + { + // HasExited can be true before all the error output has been read; this waits for the rest + await failedTool.Process.WaitForExitAsync(CancellationToken.None).ConfigureAwait(false); + Console.WriteLine($"{failedTool.Name} failed to start:"); + Console.WriteLine(failedTool.ErrorOutput.ToString()); + } + launcher.StopProcesses(); return; } - await serviceControlAvailable.ConfigureAwait(false); - if (!tokenSource.IsCancellationRequested) { var serviceControlApiUrl = $"http://localhost:{controlPort}/api"; From 5d624bf7af8dbb812eba8e30553a9cfc9008d44c Mon Sep 17 00:00:00 2001 From: Christian Date: Fri, 23 Oct 2026 18:18:20 -0600 Subject: [PATCH 09/12] Set a non-zero exit code when a platform tool fails to start When a tool fails to start, Launch() prints the error and returns normally, so the launcher process still exits with code 0. A script, CI job or IDE can't tell that startup failed. Set Environment.ExitCode to 1 on that path. This doesn't throw, because the samples call Launch() without a try/catch: an exception would add an unhandled-exception stack trace on top of the error that was just printed, without telling the user anything new. --- src/Particular.PlatformSample/PlatformLauncher.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Particular.PlatformSample/PlatformLauncher.cs b/src/Particular.PlatformSample/PlatformLauncher.cs index d06f8a4..38adaf9 100644 --- a/src/Particular.PlatformSample/PlatformLauncher.cs +++ b/src/Particular.PlatformSample/PlatformLauncher.cs @@ -113,6 +113,9 @@ await Network.WaitForHttpOk($"http://localhost:{controlPort}/api", stopWaiting: } launcher.StopProcesses(); + + // Launch() is usually the last call in the sample's Main, so this makes the process report the failure + Environment.ExitCode = 1; return; } From ace7b7ee7bb284f3f25c9160415553a688935ba6 Mon Sep 17 00:00:00 2001 From: Christian Date: Fri, 23 Oct 2026 18:18:35 -0600 Subject: [PATCH 10/12] Show the exit code and a fallback hint when no error output was captured The error output is only captured while the tool output is hidden. With showPlatformToolConsoleOutput enabled, or when a tool exits without writing to stderr, the launcher printed " failed to start:" followed by an empty line. Include the exit code in the message, and when nothing was captured, point to the console output above and the .logs folder instead. --- src/Particular.PlatformSample/PlatformLauncher.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Particular.PlatformSample/PlatformLauncher.cs b/src/Particular.PlatformSample/PlatformLauncher.cs index 38adaf9..bd2af0b 100644 --- a/src/Particular.PlatformSample/PlatformLauncher.cs +++ b/src/Particular.PlatformSample/PlatformLauncher.cs @@ -108,8 +108,13 @@ await Network.WaitForHttpOk($"http://localhost:{controlPort}/api", stopWaiting: { // HasExited can be true before all the error output has been read; this waits for the rest await failedTool.Process.WaitForExitAsync(CancellationToken.None).ConfigureAwait(false); - Console.WriteLine($"{failedTool.Name} failed to start:"); - Console.WriteLine(failedTool.ErrorOutput.ToString()); + Console.WriteLine($"{failedTool.Name} failed to start (exit code {failedTool.Process.ExitCode}):"); + + // Nothing is captured when the tool output is shown in the console, or when the tool exits without writing to stderr + var errorOutput = failedTool.ErrorOutput.ToString(); + Console.WriteLine(errorOutput.Length > 0 + ? errorOutput + : $"No error output was captured. Check the console output above or the logs in {Path.Combine(finder.Root, ".logs")}{Environment.NewLine}"); } launcher.StopProcesses(); From 0ccdcb358cf2c57a90c2281250fddf5b4810d4de Mon Sep 17 00:00:00 2001 From: Christian Date: Fri, 23 Oct 2026 18:18:52 -0600 Subject: [PATCH 11/12] Handle cancellation while waiting for ServiceControl to be available Pressing Ctrl+C (or cancelling the token passed to Launch()) during "Waiting for ServiceControl to be available" crashed the launcher with an unhandled TaskCanceledException, because WaitForHttpOk lets the cancellation exception escape. This was already the case before this PR, but the PR touches this block, so it's a cheap fix. Catch OperationCanceledException when the launcher's token was cancelled, the same way the "Press Ctrl+C" wait below already does, so the launcher continues to the normal shutdown path. --- src/Particular.PlatformSample/PlatformLauncher.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Particular.PlatformSample/PlatformLauncher.cs b/src/Particular.PlatformSample/PlatformLauncher.cs index bd2af0b..a977b10 100644 --- a/src/Particular.PlatformSample/PlatformLauncher.cs +++ b/src/Particular.PlatformSample/PlatformLauncher.cs @@ -97,8 +97,15 @@ public static async Task Launch(bool showPlatformToolConsoleOutput = false, stri launcher.ServicePulse(pulsePort, controlPort, monitoringPort, servicePulseDefaultRoute); Console.Write("Waiting for ServiceControl to be available"); - await Network.WaitForHttpOk($"http://localhost:{controlPort}/api", stopWaiting: () => launcher.FindExitedTools().Count > 0, cancellationToken: tokenSource.Token) - .ConfigureAwait(false); + try + { + await Network.WaitForHttpOk($"http://localhost:{controlPort}/api", stopWaiting: () => launcher.FindExitedTools().Count > 0, cancellationToken: tokenSource.Token) + .ConfigureAwait(false); + } + catch (OperationCanceledException) when (tokenSource.Token.IsCancellationRequested) + { + // Ctrl+C was pressed, or the caller cancelled, before ServiceControl was available + } // A platform tool exited on its own (not because of Ctrl+C), for example because the .NET runtime version it requires is not installed var failedTools = launcher.FindExitedTools(); From 1d092d0f24f9a756e832ad419dc10100d32558ff Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Mon, 26 Oct 2026 11:20:57 -0700 Subject: [PATCH 12/12] remove dup code --- .../PlatformLauncher.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/Particular.PlatformSample/PlatformLauncher.cs b/src/Particular.PlatformSample/PlatformLauncher.cs index 4ec867b..a977b10 100644 --- a/src/Particular.PlatformSample/PlatformLauncher.cs +++ b/src/Particular.PlatformSample/PlatformLauncher.cs @@ -131,22 +131,6 @@ await Network.WaitForHttpOk($"http://localhost:{controlPort}/api", stopWaiting: return; } - // A platform tool exited on its own (not because of Ctrl+C), for example because the .NET runtime version it requires is not installed - var failedTools = launcher.FindExitedTools(); - if (failedTools.Count > 0 && !tokenSource.IsCancellationRequested) - { - foreach (var failedTool in failedTools) - { - // HasExited can be true before all the error output has been read; this waits for the rest - await failedTool.Process.WaitForExitAsync(CancellationToken.None).ConfigureAwait(false); - Console.WriteLine($"{failedTool.Name} failed to start:"); - Console.WriteLine(failedTool.ErrorOutput.ToString()); - } - - launcher.StopProcesses(); - return; - } - if (!tokenSource.IsCancellationRequested) { var serviceControlApiUrl = $"http://localhost:{controlPort}/api";