Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 11 additions & 82 deletions profiler/src/Demos/Samples.BuggyBits/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
// </copyright>
using System;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Datadog.Demos.Util;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

Expand Down Expand Up @@ -56,36 +56,14 @@ public static async Task Main(string[] args)

using (var host = CreateHostBuilder(args).Build())
{
// ASP.NET Core accepts listening url via what is set by Visual Studio
// (from the launchsettings.json). It could be overriden by --Urls
// on the command line
var configuration = host.Services.GetService(typeof(IConfiguration)) as IConfiguration;
var rootUrl = configuration["urls"];

// otherwise, use the default ASP.NET Core value
if (string.IsNullOrEmpty(rootUrl))
{
rootUrl = "http://localhost:5000";
}

// avoid race condition in CI to find an available port
int port = -1;
if (int.TryParse(rootUrl.Substring(rootUrl.LastIndexOf(':') + 1), out port))
{
port = GetValidPort(port, 3);
if (port != -1)
{
rootUrl = rootUrl.Substring(0, rootUrl.LastIndexOf(':') + 1) + port;
}
}
await host.StartAsync();

var rootUrl = GetBoundRootUrl(host);
WriteLine($"Listening to {rootUrl}");

var cts = new CancellationTokenSource();
using (var selfInvoker = new SelfInvoker(cts.Token, scenario, nbIdleThreads, _disableLogs))
{
await host.StartAsync();

WriteLine();
WriteLine($"Started at {DateTime.UtcNow}.");

Expand Down Expand Up @@ -161,62 +139,13 @@ public static IHostBuilder CreateHostBuilder(string[] args) =>
}
});

public static int GetOpenPort()
{
TcpListener tcpListener = null;
try
{
tcpListener = new TcpListener(IPAddress.Loopback, 0);
tcpListener.Start();
var port = ((IPEndPoint)tcpListener.LocalEndpoint).Port;
return port;
}
finally
{
tcpListener?.Stop();
}
}

private static int GetValidPort(int initialPort, int retries)
private static string GetBoundRootUrl(IHost host)
{
var port = initialPort;
bool isPortValid = false;
while (true)
{
// seems like we can't reuse a listener if it fails to start,
// so create a new listener each time we retry
var listener = new HttpListener();
listener.Prefixes.Add($"http://127.0.0.1:{port}/");
listener.Prefixes.Add($"http://localhost:{port}/");

try
{
listener.Start();

// success
isPortValid = true;
break;
}
catch (HttpListenerException) when (retries > 0)
{
// only catch the exception if there are retries left
port = GetOpenPort();
retries--;
}
finally
{
listener.Close();
}
}

if (isPortValid)
{
return port;
}
else
{
return -1; // no valid port found
}
// Read the address Kestrel actually bound. Avoids race conditions where the caller
// passes a pre-picked port that another process grabs before we bind.
var server = (IServer)host.Services.GetService(typeof(IServer));
var address = server?.Features.Get<IServerAddressesFeature>()?.Addresses.FirstOrDefault();
return string.IsNullOrEmpty(address) ? "http://localhost:5000" : address.TrimEnd('/');
}

private static void ParseCommandLine(string[] args, out bool disableLogs, out TimeSpan timeout, out int iterations, out Scenario scenario, out int nbIdleThreads)
Expand Down
31 changes: 16 additions & 15 deletions profiler/src/Demos/Samples.Website-AspNetCore01/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@

using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Datadog.Demos.Util;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.Extensions.Hosting;

namespace Samples.Website_AspNetCore01
Expand Down Expand Up @@ -46,20 +48,6 @@ public static async Task Main(string[] args)
WriteLine($"host built in {sw.ElapsedMilliseconds} ms");
sw.Restart();

// ASP.NET Core accepts listening url via what is set by Visual Studio
// (from the launchsettings.json). It could be overriden by --Urls
// on the command line
var configuration = host.Services.GetService(typeof(IConfiguration)) as IConfiguration;
var rootUrl = configuration["Urls"];

// otherwise, use the default ASP.NET Core value
if (string.IsNullOrEmpty(rootUrl))
{
rootUrl = "http://localhost:5000";
}

WriteLine($"Listening to {rootUrl}");

var cts = new CancellationTokenSource();
using (var selfInvoker = new SelfInvoker(cts.Token))
{
Expand All @@ -72,6 +60,12 @@ public static async Task Main(string[] args)
sw.Stop();
WriteLine($"host started in {sw.ElapsedMilliseconds} ms");

// Read the address Kestrel actually bound (after StartAsync). Configuration["Urls"]
// may be "http://127.0.0.1:0" when the test runner asks for a dynamic port, so the
// bound address is the only reliable source of the real listening URL.
var rootUrl = GetBoundRootUrl(host);
WriteLine($"Listening to {rootUrl}");

WriteLine();
WriteLine($"Started at {DateTime.UtcNow}.");

Expand Down Expand Up @@ -107,6 +101,13 @@ public static IHostBuilder CreateHostBuilder(string[] args) =>
webBuilder.UseStartup<Startup>();
});

private static string GetBoundRootUrl(IHost host)
{
var server = (IServer)host.Services.GetService(typeof(IServer));
var address = server?.Features.Get<IServerAddressesFeature>()?.Addresses.FirstOrDefault();
return string.IsNullOrEmpty(address) ? "http://localhost:5000" : address.TrimEnd('/');
}

// Helper method to output both to the console (when the app runs in the console)
// and via Trace to see them while running under IISExpress both in Visual Studio
// or with SysInternals DebugView (https://docs.microsoft.com/en-us/sysinternals/downloads/debugview)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
Expand Down Expand Up @@ -151,10 +152,11 @@ private string GetApplicationPath()
throw new Exception($"Unable to find executing assembly at {applicationPath}");
}

// Look for a free open port to pass to the ASP.NET Core applications
// that accept --urls on their command line
_appListenerPort = $"http://localhost:{TcpPortProvider.GetOpenPort()}";
var arguments = $"--timeout {TestDurationInSeconds} --urls {_appListenerPort}";
// Use port 0 so Kestrel binds an OS-assigned free port and avoids the TOCTOU race
// of pre-picking a port. Bind to 127.0.0.1 explicitly: Kestrel rejects "localhost:0"
// with InvalidOperationException ("Dynamic port binding is not supported when binding
// to localhost"). The actual bound URL is parsed from stdout after the run.
var arguments = $"--timeout {TestDurationInSeconds} --urls http://127.0.0.1:0";
if (!string.IsNullOrEmpty(_commandLine))
{
arguments += $" {_commandLine}";
Expand Down Expand Up @@ -195,6 +197,7 @@ private void RunTest(MockDatadogAgent agent)
var standardOutput = processHelper.StandardOutput;
var errorOutput = processHelper.ErrorOutput;
ProcessOutput = standardOutput;
_appListenerPort = ExtractListeningUrl(standardOutput);

if (!ranToCompletion)
{
Expand Down Expand Up @@ -251,5 +254,18 @@ private void SetEnvironmentVariables(StringDictionary environmentVariables, Mock
{
Environment.PopulateEnvironmentVariables(environmentVariables, agent, ProfilingExportsIntervalInSeconds, ServiceName);
}

// Matches BuggyBits ("Listening to http://...") and ASP.NET Core's default Kestrel
// startup log ("Now listening on: http://..."). Returns null if neither is found.
private static string ExtractListeningUrl(string output)
{
if (string.IsNullOrEmpty(output))
{
return null;
}

var match = Regex.Match(output, @"(?:Listening to|Now listening on:)\s+(http://\S+)");
return match.Success ? match.Groups[1].Value.TrimEnd('/') : null;
}
}
}
Loading