1
- use std:: { collections:: HashMap , future:: Future , io:: IsTerminal , net:: SocketAddr , sync:: Arc } ;
1
+ use std:: {
2
+ collections:: HashMap ,
3
+ future:: Future ,
4
+ io:: { ErrorKind , IsTerminal } ,
5
+ net:: SocketAddr ,
6
+ sync:: Arc ,
7
+ } ;
2
8
3
9
use anyhow:: { bail, Context } ;
4
10
use http:: {
@@ -41,12 +47,16 @@ use crate::{
41
47
Body , NotFoundRouteKind , TlsConfig , TriggerApp , TriggerInstanceBuilder ,
42
48
} ;
43
49
50
+ pub const MAX_RETRIES : u16 = 10 ;
51
+
44
52
/// An HTTP server which runs Spin apps.
45
53
pub struct HttpServer < F : RuntimeFactors > {
46
54
/// The address the server is listening on.
47
55
listen_addr : SocketAddr ,
48
56
/// The TLS configuration for the server.
49
57
tls_config : Option < TlsConfig > ,
58
+ /// Whether to find a free port if the specified port is already in use.
59
+ find_free_port : bool ,
50
60
/// Request router.
51
61
router : Router ,
52
62
/// The app being triggered.
@@ -62,6 +72,7 @@ impl<F: RuntimeFactors> HttpServer<F> {
62
72
pub fn new (
63
73
listen_addr : SocketAddr ,
64
74
tls_config : Option < TlsConfig > ,
75
+ find_free_port : bool ,
65
76
trigger_app : TriggerApp < F > ,
66
77
) -> anyhow:: Result < Self > {
67
78
// This needs to be a vec before building the router to handle duplicate routes
@@ -129,6 +140,7 @@ impl<F: RuntimeFactors> HttpServer<F> {
129
140
Ok ( Self {
130
141
listen_addr,
131
142
tls_config,
143
+ find_free_port,
132
144
router,
133
145
trigger_app,
134
146
component_trigger_configs,
@@ -138,12 +150,18 @@ impl<F: RuntimeFactors> HttpServer<F> {
138
150
139
151
/// Serve incoming requests over the provided [`TcpListener`].
140
152
pub async fn serve ( self : Arc < Self > ) -> anyhow:: Result < ( ) > {
141
- let listener = TcpListener :: bind ( self . listen_addr ) . await . with_context ( || {
142
- format ! (
143
- "Unable to listen on {listen_addr}" ,
144
- listen_addr = self . listen_addr
145
- )
146
- } ) ?;
153
+ let listener: TcpListener = if self . find_free_port {
154
+ self . search_for_free_port ( ) . await ?
155
+ } else {
156
+ TcpListener :: bind ( self . listen_addr ) . await . map_err ( |err| {
157
+ if err. kind ( ) == ErrorKind :: AddrInUse {
158
+ anyhow:: anyhow!( "{} is already in use. To have Spin search for a free port, use the --find-free-port option." , self . listen_addr)
159
+ } else {
160
+ anyhow:: anyhow!( "Unable to listen on {}: {err:?}" , self . listen_addr)
161
+ }
162
+ } ) ?
163
+ } ;
164
+
147
165
if let Some ( tls_config) = self . tls_config . clone ( ) {
148
166
self . serve_https ( listener, tls_config) . await ?;
149
167
} else {
@@ -152,6 +170,37 @@ impl<F: RuntimeFactors> HttpServer<F> {
152
170
Ok ( ( ) )
153
171
}
154
172
173
+ async fn search_for_free_port ( & self ) -> anyhow:: Result < TcpListener > {
174
+ let mut found_listener = None ;
175
+ let mut addr = self . listen_addr ;
176
+
177
+ for _ in 1 ..=MAX_RETRIES {
178
+ if addr. port ( ) == u16:: MAX {
179
+ anyhow:: bail!(
180
+ "Couldn't find a free port as we've reached the maximum port number. Consider retrying with a lower base port."
181
+ ) ;
182
+ }
183
+
184
+ match TcpListener :: bind ( addr) . await {
185
+ Ok ( listener) => {
186
+ found_listener = Some ( listener) ;
187
+ break ;
188
+ }
189
+ Err ( err) if err. kind ( ) == ErrorKind :: AddrInUse => {
190
+ addr. set_port ( addr. port ( ) + 1 ) ;
191
+ continue ;
192
+ }
193
+ Err ( err) => anyhow:: bail!( "Unable to listen on {addr}: {err:?}" , ) ,
194
+ }
195
+ }
196
+
197
+ found_listener. ok_or_else ( || anyhow:: anyhow!(
198
+ "Couldn't find a free port in the range {}-{}. Consider retrying with a different base port." ,
199
+ self . listen_addr. port( ) ,
200
+ self . listen_addr. port( ) + MAX_RETRIES
201
+ ) )
202
+ }
203
+
155
204
async fn serve_http ( self : Arc < Self > , listener : TcpListener ) -> anyhow:: Result < ( ) > {
156
205
self . print_startup_msgs ( "http" , & listener) ?;
157
206
loop {
0 commit comments