From 75fcd3c707dfbaf0b8adcc6053ef43234d3dc2e8 Mon Sep 17 00:00:00 2001 From: John Simons Date: Fri, 12 Dec 2025 15:05:22 +1000 Subject: [PATCH 1/2] Include ServicePulse frontend code into ServiceControl --- src/Directory.Packages.props | 1 + .../Hosting/Commands/RunCommand.cs | 16 ++++- .../WebApi/AppConstantsMiddleware.cs | 47 +++++++++++++ .../WebApi/ServicePulseSettings.cs | 70 +++++++++++++++++++ src/ServiceControl/ServiceControl.csproj | 1 + .../WebApplicationExtensions.cs | 4 +- 6 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 src/ServiceControl/Infrastructure/WebApi/AppConstantsMiddleware.cs create mode 100644 src/ServiceControl/Infrastructure/WebApi/ServicePulseSettings.cs diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 7e49d6d805..1c237f8b61 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -61,6 +61,7 @@ + diff --git a/src/ServiceControl/Hosting/Commands/RunCommand.cs b/src/ServiceControl/Hosting/Commands/RunCommand.cs index db658857de..e344125ae4 100644 --- a/src/ServiceControl/Hosting/Commands/RunCommand.cs +++ b/src/ServiceControl/Hosting/Commands/RunCommand.cs @@ -1,8 +1,12 @@ namespace ServiceControl.Hosting.Commands { + using System; + using System.IO; + using System.Reflection; using System.Threading.Tasks; using Infrastructure.WebApi; using Microsoft.AspNetCore.Builder; + using Microsoft.Extensions.FileProviders; using NServiceBus; using Particular.ServiceControl; using Particular.ServiceControl.Hosting; @@ -23,8 +27,18 @@ public override async Task Execute(HostArguments args, Settings settings) hostBuilder.AddServiceControl(settings, endpointConfiguration); hostBuilder.AddServiceControlApi(); + var servicePulsePath = Path.Combine(AppContext.BaseDirectory, "platform", "servicepulse", "ServicePulse.dll"); + var manifestEmbeddedFileProvider = new ManifestEmbeddedFileProvider(Assembly.LoadFrom(servicePulsePath), "wwwroot"); + var fileProvider = new CompositeFileProvider(hostBuilder.Environment.WebRootFileProvider, manifestEmbeddedFileProvider); + var defaultFilesOptions = new DefaultFilesOptions { FileProvider = fileProvider }; + var staticFileOptions = new StaticFileOptions { FileProvider = fileProvider }; + var app = hostBuilder.Build(); - app.UseServiceControl(); + app.UseServiceControl() + .UseMiddleware() + .UseDefaultFiles(defaultFilesOptions) + .UseStaticFiles(staticFileOptions); + await app.RunAsync(settings.RootUrl); } } diff --git a/src/ServiceControl/Infrastructure/WebApi/AppConstantsMiddleware.cs b/src/ServiceControl/Infrastructure/WebApi/AppConstantsMiddleware.cs new file mode 100644 index 0000000000..c9232fb96e --- /dev/null +++ b/src/ServiceControl/Infrastructure/WebApi/AppConstantsMiddleware.cs @@ -0,0 +1,47 @@ +namespace ServiceControl.Infrastructure.WebApi +{ + using System.Net.Mime; + using System.Text.Json; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Http; + + class AppConstantsMiddleware + { + readonly RequestDelegate next; + readonly string content; + static AppConstantsMiddleware() => FileVersion = ServiceControlVersion.GetFileVersion(); + static readonly string FileVersion; + + public AppConstantsMiddleware(RequestDelegate next) + { + this.next = next; + + var settings = ServicePulseSettings.GetFromEnvironmentVariables(); + var constants = new + { + default_route = "/dashboard", + service_control_url = "api/", + monitoring_urls = new[] { settings.MonitoringUri.ToString() }, + showPendingRetry = settings.ShowPendingRetry, + version = FileVersion, + embedded = true + }; + var options = new JsonSerializerOptions { PropertyNamingPolicy = null }; + + content = $"window.defaultConfig = {JsonSerializer.Serialize(constants, options)}"; + } + + public async Task InvokeAsync(HttpContext context) + { + if (context.Request.Path.StartsWithSegments("/js/app.constants.js")) + { + context.Response.ContentType = MediaTypeNames.Text.JavaScript; + + await context.Response.WriteAsync(content); + return; + } + + await next(context); + } + } +} diff --git a/src/ServiceControl/Infrastructure/WebApi/ServicePulseSettings.cs b/src/ServiceControl/Infrastructure/WebApi/ServicePulseSettings.cs new file mode 100644 index 0000000000..63687480ab --- /dev/null +++ b/src/ServiceControl/Infrastructure/WebApi/ServicePulseSettings.cs @@ -0,0 +1,70 @@ +using System; +using System.Text.Json; + +class ServicePulseSettings +{ + public required Uri MonitoringUri { get; init; } + + public required string DefaultRoute { get; init; } + + public required bool ShowPendingRetry { get; init; } + + + public static ServicePulseSettings GetFromEnvironmentVariables() + { + var monitoringUrls = ParseLegacyMonitoringValue(Environment.GetEnvironmentVariable("MONITORING_URLS")); + var monitoringUrl = Environment.GetEnvironmentVariable("MONITORING_URL"); + + monitoringUrl ??= monitoringUrls; + monitoringUrl ??= "http://localhost:33633/"; + + var monitoringUri = monitoringUrl == "!" ? null : new Uri(monitoringUrl); + var defaultRoute = Environment.GetEnvironmentVariable("DEFAULT_ROUTE") ?? "/dashboard"; + + var showPendingRetryValue = Environment.GetEnvironmentVariable("SHOW_PENDING_RETRY"); + bool.TryParse(showPendingRetryValue, out var showPendingRetry); + + return new ServicePulseSettings + { + MonitoringUri = monitoringUri, + DefaultRoute = defaultRoute, + ShowPendingRetry = showPendingRetry, + }; + } + + static string ParseLegacyMonitoringValue(string value) + { + if (value is null) + { + return null; + } + + var cleanedValue = value.Replace('\'', '"'); + var json = $$"""{"Addresses":{{cleanedValue}}}"""; + + MonitoringUrls result; + + try + { + result = JsonSerializer.Deserialize(json); + } + catch (JsonException) + { + return null; + } + + var addresses = result?.Addresses; + + if (addresses is not null && addresses.Length > 0) + { + return addresses[0]; + } + + return null; + } + + class MonitoringUrls + { + public string[] Addresses { get; set; } = []; + } +} diff --git a/src/ServiceControl/ServiceControl.csproj b/src/ServiceControl/ServiceControl.csproj index 04f5956ccf..7d65a229be 100644 --- a/src/ServiceControl/ServiceControl.csproj +++ b/src/ServiceControl/ServiceControl.csproj @@ -32,6 +32,7 @@ + diff --git a/src/ServiceControl/WebApplicationExtensions.cs b/src/ServiceControl/WebApplicationExtensions.cs index dfa7511613..a6b476751b 100644 --- a/src/ServiceControl/WebApplicationExtensions.cs +++ b/src/ServiceControl/WebApplicationExtensions.cs @@ -7,7 +7,7 @@ namespace ServiceControl; public static class WebApplicationExtensions { - public static void UseServiceControl(this WebApplication app) + public static IApplicationBuilder UseServiceControl(this WebApplication app) { app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All }); app.UseResponseCompression(); @@ -16,5 +16,7 @@ public static void UseServiceControl(this WebApplication app) app.MapHub("/api/messagestream"); app.UseCors(); app.MapControllers(); + + return app; } } \ No newline at end of file From cbcac2950ae9f20973773f47641ec778d28d298e Mon Sep 17 00:00:00 2001 From: John Simons Date: Fri, 12 Dec 2025 17:56:34 +1000 Subject: [PATCH 2/2] Changes based on feedback --- .../Hosting/Commands/RunCommand.cs | 18 +++--------------- .../WebApi/AppConstantsMiddleware.cs | 2 +- .../WebApplicationExtensions.cs | 19 +++++++++++++++++++ 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/ServiceControl/Hosting/Commands/RunCommand.cs b/src/ServiceControl/Hosting/Commands/RunCommand.cs index e344125ae4..8cab478a20 100644 --- a/src/ServiceControl/Hosting/Commands/RunCommand.cs +++ b/src/ServiceControl/Hosting/Commands/RunCommand.cs @@ -1,17 +1,13 @@ namespace ServiceControl.Hosting.Commands { - using System; - using System.IO; - using System.Reflection; using System.Threading.Tasks; - using Infrastructure.WebApi; using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.FileProviders; using NServiceBus; using Particular.ServiceControl; using Particular.ServiceControl.Hosting; using ServiceBus.Management.Infrastructure.Settings; using ServiceControl; + using ServiceControl.Infrastructure.WebApi; class RunCommand : AbstractCommand { @@ -27,17 +23,9 @@ public override async Task Execute(HostArguments args, Settings settings) hostBuilder.AddServiceControl(settings, endpointConfiguration); hostBuilder.AddServiceControlApi(); - var servicePulsePath = Path.Combine(AppContext.BaseDirectory, "platform", "servicepulse", "ServicePulse.dll"); - var manifestEmbeddedFileProvider = new ManifestEmbeddedFileProvider(Assembly.LoadFrom(servicePulsePath), "wwwroot"); - var fileProvider = new CompositeFileProvider(hostBuilder.Environment.WebRootFileProvider, manifestEmbeddedFileProvider); - var defaultFilesOptions = new DefaultFilesOptions { FileProvider = fileProvider }; - var staticFileOptions = new StaticFileOptions { FileProvider = fileProvider }; - var app = hostBuilder.Build(); - app.UseServiceControl() - .UseMiddleware() - .UseDefaultFiles(defaultFilesOptions) - .UseStaticFiles(staticFileOptions); + app.UseServiceControl(); + app.UseServicePulse(); await app.RunAsync(settings.RootUrl); } diff --git a/src/ServiceControl/Infrastructure/WebApi/AppConstantsMiddleware.cs b/src/ServiceControl/Infrastructure/WebApi/AppConstantsMiddleware.cs index c9232fb96e..483c455961 100644 --- a/src/ServiceControl/Infrastructure/WebApi/AppConstantsMiddleware.cs +++ b/src/ServiceControl/Infrastructure/WebApi/AppConstantsMiddleware.cs @@ -19,7 +19,7 @@ public AppConstantsMiddleware(RequestDelegate next) var settings = ServicePulseSettings.GetFromEnvironmentVariables(); var constants = new { - default_route = "/dashboard", + default_route = settings.DefaultRoute, service_control_url = "api/", monitoring_urls = new[] { settings.MonitoringUri.ToString() }, showPendingRetry = settings.ShowPendingRetry, diff --git a/src/ServiceControl/WebApplicationExtensions.cs b/src/ServiceControl/WebApplicationExtensions.cs index a6b476751b..bbc35b5a29 100644 --- a/src/ServiceControl/WebApplicationExtensions.cs +++ b/src/ServiceControl/WebApplicationExtensions.cs @@ -1,9 +1,13 @@ namespace ServiceControl; +using System; +using System.IO; +using System.Reflection; using Infrastructure.SignalR; using Infrastructure.WebApi; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.FileProviders; public static class WebApplicationExtensions { @@ -19,4 +23,19 @@ public static IApplicationBuilder UseServiceControl(this WebApplication app) return app; } + + public static IApplicationBuilder UseServicePulse(this WebApplication app) + { + var servicePulsePath = Path.Combine(AppContext.BaseDirectory, "platform", "servicepulse", "ServicePulse.dll"); + var manifestEmbeddedFileProvider = new ManifestEmbeddedFileProvider(Assembly.LoadFrom(servicePulsePath), "wwwroot"); + var fileProvider = new CompositeFileProvider(app.Environment.WebRootFileProvider, manifestEmbeddedFileProvider); + var defaultFilesOptions = new DefaultFilesOptions { FileProvider = fileProvider }; + var staticFileOptions = new StaticFileOptions { FileProvider = fileProvider }; + + app.UseMiddleware() + .UseDefaultFiles(defaultFilesOptions) + .UseStaticFiles(staticFileOptions); + + return app; + } } \ No newline at end of file