diff --git a/src/Particular.PlatformSample/AppLauncher.cs b/src/Particular.PlatformSample/AppLauncher.cs index 8d96453..0346054 100644 --- a/src/Particular.PlatformSample/AppLauncher.cs +++ b/src/Particular.PlatformSample/AppLauncher.cs @@ -15,6 +15,26 @@ sealed class AppLauncher(bool showPlatformToolConsoleOutput) : IAsyncDisposable readonly string platformPath = Path.Combine(AppContext.BaseDirectory, "platform"); readonly bool hideConsoleOutput = !showPlatformToolConsoleOutput; readonly Stack> cleanupActions = new(); + readonly List platformTools = []; + + public sealed record PlatformTool(string Name, Process Process, StringBuilder ErrorOutput); + + public void StopProcesses() + { + foreach (var tool in platformTools) + { + try + { + tool.Process.Kill(entireProcessTree: true); + } + catch (InvalidOperationException) + { + // The process has already exited + } + } + } + + public List FindExitedTools() => platformTools.FindAll(tool => tool.Process.HasExited); public async Task RavenDB(string logsPath, string dataDirectory, CancellationToken cancellationToken = default) { @@ -101,7 +121,7 @@ public void ServiceControl(int port, int maintenancePort, string logPath, string return ValueTask.CompletedTask; }); - StartProcess(Path.Combine(platformPath, "servicecontrol", "servicecontrol-instance", "ServiceControl.dll")); + StartProcess("ServiceControl", Path.Combine(platformPath, "servicecontrol", "servicecontrol-instance", "ServiceControl.dll")); } public void ServiceControlAudit(int port, int maintenancePort, string logPath, string transportPath, Uri connectionString) @@ -123,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) @@ -143,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) @@ -160,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); } - void StartProcess(string assemblyPath, Dictionary environmentVariables = null) + void StartProcess(string name, string assemblyPath, Dictionary environmentVariables = null) { var workingDirectory = Path.GetDirectoryName(assemblyPath)!; var startInfo = new ProcessStartInfo("dotnet") @@ -190,13 +210,25 @@ void StartProcess(string assemblyPath, Dictionary environmentVar } 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 tool fails to start + process.ErrorDataReceived += (_, args) => + { + if (args.Data is not null) + { + tool.ErrorOutput.AppendLine(args.Data); + } + }; + process.BeginErrorReadLine(); process.BeginOutputReadLine(); } + + 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 dfa2d30..d06f8a4 100644 --- a/src/Particular.PlatformSample/PlatformLauncher.cs +++ b/src/Particular.PlatformSample/PlatformLauncher.cs @@ -97,9 +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"); - await Network.WaitForHttpOk($"http://localhost:{controlPort}/api", cancellationToken: tokenSource.Token) + await Network.WaitForHttpOk($"http://localhost:{controlPort}/api", stopWaiting: () => launcher.FindExitedTools().Count > 0, cancellationToken: tokenSource.Token) .ConfigureAwait(false); + // 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";