1
1
// Licensed to the .NET Foundation under one or more agreements.
2
2
// The .NET Foundation licenses this file to you under the MIT license.
3
3
4
+ using System . Net ;
5
+ using System . Net . Sockets ;
4
6
using Microsoft . Extensions . Logging ;
5
7
6
8
namespace Microsoft . AspNetCore . Testing ;
7
9
8
10
public static class ServerRetryHelper
9
11
{
12
+ private const int RetryCount = 10 ;
13
+
10
14
/// <summary>
11
15
/// Retry a func. Useful when a test needs an explicit port and you want to avoid port conflicts.
12
16
/// </summary>
13
17
public static async Task BindPortsWithRetry ( Func < int , Task > retryFunc , ILogger logger )
14
18
{
19
+ var ports = GetFreePorts ( RetryCount ) ;
20
+
15
21
var retryCount = 0 ;
16
22
while ( true )
17
23
{
18
- // Approx dynamic port range on Windows and Linux.
19
- var randomPort = Random . Shared . Next ( 35000 , 60000 ) ;
20
24
21
25
try
22
26
{
23
- await retryFunc ( randomPort ) ;
27
+ await retryFunc ( ports [ retryCount ] ) ;
24
28
break ;
25
29
}
26
30
catch ( Exception ex )
27
31
{
28
32
retryCount ++ ;
29
33
30
- if ( retryCount >= 5 )
34
+ if ( retryCount >= RetryCount )
31
35
{
32
36
throw ;
33
37
}
@@ -38,4 +42,35 @@ public static async Task BindPortsWithRetry(Func<int, Task> retryFunc, ILogger l
38
42
}
39
43
}
40
44
}
45
+
46
+ private static int [ ] GetFreePorts ( int count )
47
+ {
48
+ var sockets = new List < Socket > ( ) ;
49
+
50
+ for ( var i = 0 ; i < count ; i ++ )
51
+ {
52
+ // Find a port that's free by binding port 0.
53
+ // Note that this port should be free when the test runs, but:
54
+ // - Something else could steal it before the test uses it.
55
+ // - UDP port with the same number could be in use.
56
+ // For that reason, some retries should be available.
57
+ var ipEndPoint = new IPEndPoint ( IPAddress . Loopback , 0 ) ;
58
+ var listenSocket = new Socket ( ipEndPoint . AddressFamily , SocketType . Stream , ProtocolType . Tcp ) ;
59
+
60
+ listenSocket . Bind ( ipEndPoint ) ;
61
+
62
+ sockets . Add ( listenSocket ) ;
63
+ }
64
+
65
+ // Ports are calculated upfront. Rebinding with port 0 could result the same port
66
+ // being returned for each retry.
67
+ var ports = sockets . Select ( s => ( IPEndPoint ) s . LocalEndPoint ) . Select ( ep => ep . Port ) . ToArray ( ) ;
68
+
69
+ foreach ( var socket in sockets )
70
+ {
71
+ socket . Dispose ( ) ;
72
+ }
73
+
74
+ return ports ;
75
+ }
41
76
}
0 commit comments