33// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2022 Datadog, Inc.
44// </copyright>
55using System ;
6- using System . Collections . Generic ;
76using System . Diagnostics ;
8- using System . Net . Sockets ;
7+ using System . Linq ;
98using System . Threading ;
109using System . Threading . Tasks ;
1110using Datadog . Demos . Util ;
1211using Microsoft . AspNetCore . Hosting ;
1312using Microsoft . AspNetCore . Hosting . Server ;
1413using Microsoft . AspNetCore . Hosting . Server . Features ;
15- using Microsoft . Extensions . Configuration ;
1614using Microsoft . Extensions . Hosting ;
1715using 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 )
0 commit comments