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+ } ;
28
39use anyhow:: { bail, Context } ;
410use http:: {
@@ -41,12 +47,16 @@ use crate::{
4147 Body , NotFoundRouteKind , TlsConfig , TriggerApp , TriggerInstanceBuilder ,
4248} ;
4349
50+ pub const MAX_RETRIES : u16 = 10 ;
51+
4452/// An HTTP server which runs Spin apps.
4553pub struct HttpServer < F : RuntimeFactors > {
4654 /// The address the server is listening on.
4755 listen_addr : SocketAddr ,
4856 /// The TLS configuration for the server.
4957 tls_config : Option < TlsConfig > ,
58+ /// Whether to find a free port if the specified port is already in use.
59+ find_free_port : bool ,
5060 /// Request router.
5161 router : Router ,
5262 /// The app being triggered.
@@ -62,6 +72,7 @@ impl<F: RuntimeFactors> HttpServer<F> {
6272 pub fn new (
6373 listen_addr : SocketAddr ,
6474 tls_config : Option < TlsConfig > ,
75+ find_free_port : bool ,
6576 trigger_app : TriggerApp < F > ,
6677 ) -> anyhow:: Result < Self > {
6778 // This needs to be a vec before building the router to handle duplicate routes
@@ -129,6 +140,7 @@ impl<F: RuntimeFactors> HttpServer<F> {
129140 Ok ( Self {
130141 listen_addr,
131142 tls_config,
143+ find_free_port,
132144 router,
133145 trigger_app,
134146 component_trigger_configs,
@@ -138,12 +150,18 @@ impl<F: RuntimeFactors> HttpServer<F> {
138150
139151 /// Serve incoming requests over the provided [`TcpListener`].
140152 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+
147165 if let Some ( tls_config) = self . tls_config . clone ( ) {
148166 self . serve_https ( listener, tls_config) . await ?;
149167 } else {
@@ -152,6 +170,37 @@ impl<F: RuntimeFactors> HttpServer<F> {
152170 Ok ( ( ) )
153171 }
154172
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+
155204 async fn serve_http ( self : Arc < Self > , listener : TcpListener ) -> anyhow:: Result < ( ) > {
156205 self . print_startup_msgs ( "http" , & listener) ?;
157206 loop {
0 commit comments