11using Microsoft . AspNetCore . SignalR ;
2+ using Microsoft . AspNetCore . Hosting . Server ;
3+ using Microsoft . AspNetCore . Hosting . Server . Features ;
24using PerfProblemSimulator . Hubs ;
35using System . Diagnostics ;
46using System . Net . Http ;
@@ -44,10 +46,12 @@ public class LatencyProbeService : IHostedService, IDisposable
4446 private readonly IHttpClientFactory _httpClientFactory ;
4547 private readonly ILogger < LatencyProbeService > _logger ;
4648 private readonly IConfiguration _configuration ;
49+ private readonly IServer _server ;
4750
4851 private Thread ? _probeThread ;
4952 private CancellationTokenSource ? _cts ;
5053 private bool _disposed ;
54+ private string ? _baseUrl ;
5155
5256 /// <summary>
5357 /// Probe interval in milliseconds. 100ms provides good granularity for
@@ -68,19 +72,24 @@ public LatencyProbeService(
6872 IHubContext < MetricsHub , IMetricsClient > hubContext ,
6973 IHttpClientFactory httpClientFactory ,
7074 ILogger < LatencyProbeService > logger ,
71- IConfiguration configuration )
75+ IConfiguration configuration ,
76+ IServer server )
7277 {
7378 _hubContext = hubContext ?? throw new ArgumentNullException ( nameof ( hubContext ) ) ;
7479 _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException ( nameof ( httpClientFactory ) ) ;
7580 _logger = logger ?? throw new ArgumentNullException ( nameof ( logger ) ) ;
7681 _configuration = configuration ?? throw new ArgumentNullException ( nameof ( configuration ) ) ;
82+ _server = server ?? throw new ArgumentNullException ( nameof ( server ) ) ;
7783 }
7884
7985 /// <inheritdoc />
8086 public Task StartAsync ( CancellationToken cancellationToken )
8187 {
8288 _cts = new CancellationTokenSource ( ) ;
8389
90+ // Get the server's actual listening address
91+ _baseUrl = GetProbeBaseUrl ( ) ;
92+
8493 // Create a dedicated thread (not from thread pool) for reliable probing
8594 _probeThread = new Thread ( ProbeLoop )
8695 {
@@ -90,9 +99,10 @@ public Task StartAsync(CancellationToken cancellationToken)
9099 _probeThread . Start ( _cts . Token ) ;
91100
92101 _logger . LogInformation (
93- "Latency probe service started. Interval: {Interval}ms, Timeout: {Timeout}ms" ,
102+ "Latency probe service started. Interval: {Interval}ms, Timeout: {Timeout}ms, Target: {BaseUrl} " ,
94103 ProbeIntervalMs ,
95- RequestTimeoutMs ) ;
104+ RequestTimeoutMs ,
105+ _baseUrl ) ;
96106
97107 return Task . CompletedTask ;
98108 }
@@ -116,8 +126,11 @@ private void ProbeLoop(object? state)
116126 {
117127 var cancellationToken = ( CancellationToken ) state ! ;
118128
119- // Get the base URL from configuration or use default
120- var baseUrl = GetProbeBaseUrl ( ) ;
129+ // Wait a moment for the server to fully start
130+ Thread . Sleep ( 2000 ) ;
131+
132+ // Get the base URL (may need to refresh after server starts)
133+ var baseUrl = _baseUrl ?? GetProbeBaseUrl ( ) ;
121134 _logger . LogInformation ( "Latency probe targeting: {BaseUrl}/api/health/probe" , baseUrl ) ;
122135
123136 // Create HttpClient with timeout
@@ -228,15 +241,37 @@ private void BroadcastLatency(LatencyMeasurement measurement)
228241 /// </summary>
229242 private string GetProbeBaseUrl ( )
230243 {
231- // Try to get from configuration, otherwise use sensible defaults
244+ // Try to get the actual server addresses from IServer
245+ try
246+ {
247+ var addressFeature = _server . Features . Get < IServerAddressesFeature > ( ) ;
248+ if ( addressFeature ? . Addresses . Count > 0 )
249+ {
250+ // Prefer http over https for local probing (avoid SSL overhead)
251+ var httpAddress = addressFeature . Addresses . FirstOrDefault ( a => a . StartsWith ( "http://" ) ) ;
252+ if ( ! string . IsNullOrEmpty ( httpAddress ) )
253+ {
254+ // Replace wildcard with localhost
255+ return httpAddress . Replace ( "*" , "localhost" ) . Replace ( "+" , "localhost" ) . Replace ( "[::]" , "localhost" ) ;
256+ }
257+
258+ var firstAddress = addressFeature . Addresses . First ( ) ;
259+ return firstAddress . Replace ( "*" , "localhost" ) . Replace ( "+" , "localhost" ) . Replace ( "[::]" , "localhost" ) ;
260+ }
261+ }
262+ catch ( Exception ex )
263+ {
264+ _logger . LogWarning ( ex , "Could not get server addresses from IServer feature" ) ;
265+ }
266+
267+ // Try to get from configuration
232268 var urls = _configuration [ "Urls" ] ;
233269 if ( ! string . IsNullOrEmpty ( urls ) )
234270 {
235- // Take the first URL if multiple are configured
236271 var firstUrl = urls . Split ( ';' ) [ 0 ] . Trim ( ) ;
237272 if ( ! string . IsNullOrEmpty ( firstUrl ) )
238273 {
239- return firstUrl ;
274+ return firstUrl . Replace ( "*" , "localhost" ) . Replace ( "+" , "localhost" ) ;
240275 }
241276 }
242277
@@ -247,12 +282,19 @@ private string GetProbeBaseUrl()
247282 var firstUrl = envUrls . Split ( ';' ) [ 0 ] . Trim ( ) ;
248283 if ( ! string . IsNullOrEmpty ( firstUrl ) )
249284 {
250- return firstUrl ;
285+ return firstUrl . Replace ( "*" , "localhost" ) . Replace ( "+" , "localhost" ) ;
251286 }
252287 }
253288
254- // Default to localhost on standard ports
255- return "http://localhost:5000" ;
289+ // Check applicationUrl from launchSettings (common development scenario)
290+ var appUrl = _configuration [ "applicationUrl" ] ;
291+ if ( ! string . IsNullOrEmpty ( appUrl ) )
292+ {
293+ return appUrl . Split ( ';' ) [ 0 ] . Trim ( ) ;
294+ }
295+
296+ // Default to localhost on the common development port
297+ return "http://localhost:5221" ;
256298 }
257299
258300 /// <inheritdoc />
0 commit comments