From 36917e9e8e658c466485fd2c69b4e1b0b4541f0a Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Mon, 19 Oct 2026 20:10:23 -0700 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 dcdfbcbce72038748b0de474a90da03a223aa78c Mon Sep 17 00:00:00 2001 From: Bob Langley Date: Fri, 23 Oct 2026 15:55:27 -0700 Subject: [PATCH 8/9] Bound captured ServiceControl stderr --- src/Particular.PlatformSample/AppLauncher.cs | 4 +- .../BoundedOutput.cs | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 src/Particular.PlatformSample/BoundedOutput.cs diff --git a/src/Particular.PlatformSample/AppLauncher.cs b/src/Particular.PlatformSample/AppLauncher.cs index fe9f642..8083845 100644 --- a/src/Particular.PlatformSample/AppLauncher.cs +++ b/src/Particular.PlatformSample/AppLauncher.cs @@ -15,7 +15,7 @@ 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 BoundedOutput serviceControlErrorOutput = new(); readonly List processes = []; public string ServiceControlErrorOutput => serviceControlErrorOutput.ToString(); @@ -182,7 +182,7 @@ public void ServicePulse(int port, int serviceControlPort, int monitoringPort, s StartProcess(Path.Combine(platformPath, "servicepulse", "ServicePulse.dll"), environmentVariables); } - Process StartProcess(string assemblyPath, Dictionary environmentVariables = null, StringBuilder errorOutput = null) + Process StartProcess(string assemblyPath, Dictionary environmentVariables = null, BoundedOutput errorOutput = null) { var workingDirectory = Path.GetDirectoryName(assemblyPath)!; var startInfo = new ProcessStartInfo("dotnet") diff --git a/src/Particular.PlatformSample/BoundedOutput.cs b/src/Particular.PlatformSample/BoundedOutput.cs new file mode 100644 index 0000000..aca80c6 --- /dev/null +++ b/src/Particular.PlatformSample/BoundedOutput.cs @@ -0,0 +1,46 @@ +namespace Particular +{ + using System; + using System.Text; + + sealed class BoundedOutput + { + const int MaximumLength = 8192; + readonly StringBuilder output = new(MaximumLength); + + public void AppendLine(string line) + { + lock (output) + { + Append(line.AsSpan()); + Append(Environment.NewLine.AsSpan()); + } + } + + void Append(ReadOnlySpan text) + { + if (text.Length >= MaximumLength) + { + output.Clear(); + output.Append(text[^MaximumLength..]); + return; + } + + var excess = output.Length + text.Length - MaximumLength; + if (excess > 0) + { + output.Remove(0, excess); + } + + output.Append(text); + } + + public override string ToString() + { + lock (output) + { + return output.ToString(); + } + } + } +} From 00fa72a9ddded49fd8112091e1eb8d76e123093a Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Fri, 23 Oct 2026 17:15:07 -0700 Subject: [PATCH 9/9] 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";