Skip to content

Commit ce4cc4d

Browse files
committed
Attempt to simplify the buggy bits port checking
1 parent 61267f3 commit ce4cc4d

2 files changed

Lines changed: 30 additions & 147 deletions

File tree

profiler/src/Demos/Samples.BuggyBits/Program.cs

Lines changed: 12 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,14 @@
33
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2022 Datadog, Inc.
44
// </copyright>
55
using System;
6-
using System.Collections.Generic;
76
using System.Diagnostics;
8-
using System.Net.Sockets;
7+
using System.Linq;
98
using System.Threading;
109
using System.Threading.Tasks;
1110
using Datadog.Demos.Util;
1211
using Microsoft.AspNetCore.Hosting;
1312
using Microsoft.AspNetCore.Hosting.Server;
1413
using Microsoft.AspNetCore.Hosting.Server.Features;
15-
using Microsoft.Extensions.Configuration;
1614
using Microsoft.Extensions.Hosting;
1715
using Microsoft.Extensions.Logging;
1816

@@ -44,8 +42,6 @@ public enum Scenario
4442

4543
public class Program
4644
{
47-
private const int HostBindRetries = 5;
48-
4945
private static bool _disableLogs = false;
5046

5147
public static async Task Main(string[] args)
@@ -58,8 +54,10 @@ public static async Task Main(string[] args)
5854

5955
ParseCommandLine(args, out _disableLogs, out var timeout, out var iterations, out var scenario, out var nbIdleThreads);
6056

61-
using (var host = await StartHostWithPortResilience(args))
57+
using (var host = CreateHostBuilder(args).Build())
6258
{
59+
await host.StartAsync();
60+
6361
var rootUrl = GetBoundRootUrl(host);
6462
WriteLine($"Listening to {rootUrl}");
6563

@@ -127,116 +125,8 @@ public static async Task Main(string[] args)
127125
WriteLine($"The application exited after: {sw.Elapsed} at {DateTime.UtcNow}");
128126
}
129127

130-
private static async Task<IHost> StartHostWithPortResilience(string[] args)
131-
{
132-
var host = CreateHostBuilder(args).Build();
133-
try
134-
{
135-
var rootUrl = GetConfiguredRootUrl(host);
136-
137-
for (var remainingRetries = HostBindRetries; ; remainingRetries--)
138-
{
139-
try
140-
{
141-
WriteLine($"Starting web host at {rootUrl}");
142-
await host.StartAsync();
143-
return host;
144-
}
145-
catch (Exception ex) when (remainingRetries > 0 && IsAddressInUseException(ex))
146-
{
147-
var retryRootUrl = GetUrlWithNewPort(rootUrl);
148-
WriteLine($"Address already in use for {rootUrl}. Retrying on {retryRootUrl}.");
149-
150-
host.Dispose();
151-
rootUrl = retryRootUrl;
152-
host = CreateHostBuilder(args, rootUrl).Build();
153-
}
154-
}
155-
}
156-
catch
157-
{
158-
host.Dispose();
159-
throw;
160-
}
161-
}
162-
163-
private static string GetConfiguredRootUrl(IHost host)
164-
{
165-
// ASP.NET Core accepts listening urls from launchSettings.json, ASPNETCORE_URLS,
166-
// or --urls. If none are supplied, Kestrel uses this default.
167-
var configuration = host.Services.GetService(typeof(IConfiguration)) as IConfiguration;
168-
var rootUrl = configuration?["urls"];
169-
170-
if (string.IsNullOrEmpty(rootUrl))
171-
{
172-
rootUrl = "http://localhost:5000";
173-
}
174-
175-
return GetFirstUrl(rootUrl);
176-
}
177-
178-
private static string GetBoundRootUrl(IHost host)
179-
{
180-
var server = host.Services.GetService(typeof(IServer)) as IServer;
181-
var addresses = server?.Features.Get<IServerAddressesFeature>()?.Addresses;
182-
183-
if (addresses != null)
184-
{
185-
foreach (var address in addresses)
186-
{
187-
if (!string.IsNullOrEmpty(address))
188-
{
189-
return address.TrimEnd('/');
190-
}
191-
}
192-
}
193-
194-
return GetConfiguredRootUrl(host);
195-
}
196-
197-
private static string GetFirstUrl(string urls)
198-
{
199-
var separatorIndex = urls.IndexOf(';');
200-
if (separatorIndex >= 0)
201-
{
202-
urls = urls.Substring(0, separatorIndex);
203-
}
204-
205-
return urls.TrimEnd('/');
206-
}
207-
208-
private static string GetUrlWithNewPort(string rootUrl)
209-
{
210-
// Avoid UriBuilder here: ASP.NET Core wildcard hosts (http://*:5000, http://+:5000)
211-
// are not valid Uri hosts and would throw UriFormatException. Since the retry always
212-
// rebinds to 127.0.0.1 with an OS-assigned port, only the scheme needs to be preserved.
213-
var schemeSeparator = rootUrl.IndexOf("://", StringComparison.Ordinal);
214-
var scheme = schemeSeparator > 0 ? rootUrl.Substring(0, schemeSeparator) : "http";
215-
return $"{scheme}://127.0.0.1:0";
216-
}
217-
218-
private static bool IsAddressInUseException(Exception exception)
219-
{
220-
while (exception != null)
221-
{
222-
if (exception is SocketException socketException && socketException.SocketErrorCode == SocketError.AddressAlreadyInUse)
223-
{
224-
return true;
225-
}
226-
227-
if (exception.Message.IndexOf("address already in use", StringComparison.OrdinalIgnoreCase) >= 0)
228-
{
229-
return true;
230-
}
231-
232-
exception = exception.InnerException;
233-
}
234-
235-
return false;
236-
}
237-
238-
public static IHostBuilder CreateHostBuilder(string[] args, string rootUrl = null) =>
239-
Host.CreateDefaultBuilder(GetArgsWithUrlsOverride(args, rootUrl))
128+
public static IHostBuilder CreateHostBuilder(string[] args) =>
129+
Host.CreateDefaultBuilder(args)
240130
.ConfigureWebHostDefaults(webBuilder =>
241131
{
242132
webBuilder.UseStartup<Startup>();
@@ -249,34 +139,13 @@ public static IHostBuilder CreateHostBuilder(string[] args, string rootUrl = nul
249139
}
250140
});
251141

252-
private static string[] GetArgsWithUrlsOverride(string[] args, string rootUrl)
142+
private static string GetBoundRootUrl(IHost host)
253143
{
254-
if (string.IsNullOrEmpty(rootUrl))
255-
{
256-
return args;
257-
}
258-
259-
var effectiveArgs = new List<string>(args.Length + 2);
260-
for (var i = 0; i < args.Length; i++)
261-
{
262-
var arg = args[i];
263-
if (string.Equals(arg, "--urls", StringComparison.OrdinalIgnoreCase))
264-
{
265-
i++;
266-
continue;
267-
}
268-
269-
if (arg.StartsWith("--urls=", StringComparison.OrdinalIgnoreCase))
270-
{
271-
continue;
272-
}
273-
274-
effectiveArgs.Add(arg);
275-
}
276-
277-
effectiveArgs.Add("--urls");
278-
effectiveArgs.Add(rootUrl);
279-
return effectiveArgs.ToArray();
144+
// Read the address Kestrel actually bound. Avoids race conditions where the caller
145+
// passes a pre-picked port that another process grabs before we bind.
146+
var server = (IServer)host.Services.GetService(typeof(IServer));
147+
var address = server?.Features.Get<IServerAddressesFeature>()?.Addresses.FirstOrDefault();
148+
return string.IsNullOrEmpty(address) ? "http://localhost:5000" : address.TrimEnd('/');
280149
}
281150

282151
private static void ParseCommandLine(string[] args, out bool disableLogs, out TimeSpan timeout, out int iterations, out Scenario scenario, out int nbIdleThreads)

profiler/test/Datadog.Profiler.IntegrationTests/Helpers/TestApplicationRunner.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Collections.Specialized;
88
using System.Diagnostics;
99
using System.IO;
10+
using System.Text.RegularExpressions;
1011
using Xunit;
1112
using Xunit.Abstractions;
1213
using Xunit.Sdk;
@@ -151,10 +152,9 @@ private string GetApplicationPath()
151152
throw new Exception($"Unable to find executing assembly at {applicationPath}");
152153
}
153154

154-
// Look for a free open port to pass to the ASP.NET Core applications
155-
// that accept --urls on their command line
156-
_appListenerPort = $"http://localhost:{TcpPortProvider.GetOpenPort()}";
157-
var arguments = $"--timeout {TestDurationInSeconds} --urls {_appListenerPort}";
155+
// Use port 0 so Kestrel binds an OS-assigned free port and avoids the TOCTOU race
156+
// of pre-picking a port. The actual bound URL is parsed from stdout after the run.
157+
var arguments = $"--timeout {TestDurationInSeconds} --urls http://localhost:0";
158158
if (!string.IsNullOrEmpty(_commandLine))
159159
{
160160
arguments += $" {_commandLine}";
@@ -195,6 +195,7 @@ private void RunTest(MockDatadogAgent agent)
195195
var standardOutput = processHelper.StandardOutput;
196196
var errorOutput = processHelper.ErrorOutput;
197197
ProcessOutput = standardOutput;
198+
_appListenerPort = ExtractListeningUrl(standardOutput);
198199

199200
if (!ranToCompletion)
200201
{
@@ -251,5 +252,18 @@ private void SetEnvironmentVariables(StringDictionary environmentVariables, Mock
251252
{
252253
Environment.PopulateEnvironmentVariables(environmentVariables, agent, ProfilingExportsIntervalInSeconds, ServiceName);
253254
}
255+
256+
// Matches BuggyBits ("Listening to http://...") and ASP.NET Core's default Kestrel
257+
// startup log ("Now listening on: http://..."). Returns null if neither is found.
258+
private static string ExtractListeningUrl(string output)
259+
{
260+
if (string.IsNullOrEmpty(output))
261+
{
262+
return null;
263+
}
264+
265+
var match = Regex.Match(output, @"(?:Listening to|Now listening on:)\s+(http://\S+)");
266+
return match.Success ? match.Groups[1].Value.TrimEnd('/') : null;
267+
}
254268
}
255269
}

0 commit comments

Comments
 (0)