From dfbef347742436e548e52ddf8b15dc06411bc972 Mon Sep 17 00:00:00 2001 From: Aminu 'Seun Joshua <34725212+seun-ja@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:33:08 +0100 Subject: [PATCH] adding custom port in cli opt-in Signed-off-by: Aminu 'Seun Joshua <34725212+seun-ja@users.noreply.github.com> Signed-off-by: Aminu 'Seun Joshua approach revamp using --find-free-port flag Signed-off-by: Aminu 'Seun Joshua <34725212+seun-ja@users.noreply.github.com> Signed-off-by: Aminu 'Seun Joshua improvement: readability and refactor Signed-off-by: Aminu 'Seun Joshua using bail instead Signed-off-by: Aminu 'Seun Joshua fix: bitwise logic error Signed-off-by: Aminu 'Seun Joshua refactor Signed-off-by: Aminu 'Seun Joshua refactor: cleaner logic Signed-off-by: Aminu 'Seun Joshua refactor: re-wording + logic Signed-off-by: Aminu 'Seun Joshua --- crates/trigger-http/src/lib.rs | 23 ++++++- crates/trigger-http/src/server.rs | 63 ++++++++++++++++--- .../src/runtimes/in_process_spin.rs | 2 +- 3 files changed, 78 insertions(+), 10 deletions(-) diff --git a/crates/trigger-http/src/lib.rs b/crates/trigger-http/src/lib.rs index bc004dc0df..67a69d28ee 100644 --- a/crates/trigger-http/src/lib.rs +++ b/crates/trigger-http/src/lib.rs @@ -50,6 +50,9 @@ pub struct CliArgs { /// The path to the certificate key to use for https, if this is not set, normal http will be used. The key should be in PKCS#8 format #[clap(long, env = "SPIN_TLS_KEY", requires = "tls-cert")] pub tls_key: Option, + + #[clap(long = "find-free-port")] + pub find_free_port: bool, } impl CliArgs { @@ -73,6 +76,7 @@ pub struct HttpTrigger { /// If the port is set to 0, the actual address will be determined by the OS. listen_addr: SocketAddr, tls_config: Option, + find_free_port: bool, } impl Trigger for HttpTrigger { @@ -82,7 +86,14 @@ impl Trigger for HttpTrigger { type InstanceState = (); fn new(cli_args: Self::CliArgs, app: &spin_app::App) -> anyhow::Result { - Self::new(app, cli_args.address, cli_args.into_tls_config()) + let find_free_port = cli_args.find_free_port; + + Self::new( + app, + cli_args.address, + cli_args.into_tls_config(), + find_free_port, + ) } async fn run(self, trigger_app: TriggerApp) -> anyhow::Result<()> { @@ -104,12 +115,14 @@ impl HttpTrigger { app: &spin_app::App, listen_addr: SocketAddr, tls_config: Option, + find_free_port: bool, ) -> anyhow::Result { Self::validate_app(app)?; Ok(Self { listen_addr, tls_config, + find_free_port, }) } @@ -121,8 +134,14 @@ impl HttpTrigger { let Self { listen_addr, tls_config, + find_free_port, } = self; - let server = Arc::new(HttpServer::new(listen_addr, tls_config, trigger_app)?); + let server = Arc::new(HttpServer::new( + listen_addr, + tls_config, + find_free_port, + trigger_app, + )?); Ok(server) } diff --git a/crates/trigger-http/src/server.rs b/crates/trigger-http/src/server.rs index 92cdd4e84e..25273e330e 100644 --- a/crates/trigger-http/src/server.rs +++ b/crates/trigger-http/src/server.rs @@ -1,4 +1,10 @@ -use std::{collections::HashMap, future::Future, io::IsTerminal, net::SocketAddr, sync::Arc}; +use std::{ + collections::HashMap, + future::Future, + io::{ErrorKind, IsTerminal}, + net::SocketAddr, + sync::Arc, +}; use anyhow::{bail, Context}; use http::{ @@ -41,12 +47,16 @@ use crate::{ Body, NotFoundRouteKind, TlsConfig, TriggerApp, TriggerInstanceBuilder, }; +pub const MAX_RETRIES: u16 = 10; + /// An HTTP server which runs Spin apps. pub struct HttpServer { /// The address the server is listening on. listen_addr: SocketAddr, /// The TLS configuration for the server. tls_config: Option, + /// Whether to find a free port if the specified port is already in use. + find_free_port: bool, /// Request router. router: Router, /// The app being triggered. @@ -62,6 +72,7 @@ impl HttpServer { pub fn new( listen_addr: SocketAddr, tls_config: Option, + find_free_port: bool, trigger_app: TriggerApp, ) -> anyhow::Result { // This needs to be a vec before building the router to handle duplicate routes @@ -129,6 +140,7 @@ impl HttpServer { Ok(Self { listen_addr, tls_config, + find_free_port, router, trigger_app, component_trigger_configs, @@ -138,12 +150,18 @@ impl HttpServer { /// Serve incoming requests over the provided [`TcpListener`]. pub async fn serve(self: Arc) -> anyhow::Result<()> { - let listener = TcpListener::bind(self.listen_addr).await.with_context(|| { - format!( - "Unable to listen on {listen_addr}", - listen_addr = self.listen_addr - ) - })?; + let listener: TcpListener = if self.find_free_port { + self.search_for_free_port().await? + } else { + TcpListener::bind(self.listen_addr).await.map_err(|err| { + if err.kind() == ErrorKind::AddrInUse { + anyhow::anyhow!("{} is already in use. To have Spin search for a free port, use the --find-free-port option.", self.listen_addr) + } else { + anyhow::anyhow!("Unable to listen on {}: {err:?}", self.listen_addr) + } + })? + }; + if let Some(tls_config) = self.tls_config.clone() { self.serve_https(listener, tls_config).await?; } else { @@ -152,6 +170,37 @@ impl HttpServer { Ok(()) } + async fn search_for_free_port(&self) -> anyhow::Result { + let mut found_listener = None; + let mut addr = self.listen_addr; + + for _ in 1..=MAX_RETRIES { + if addr.port() == u16::MAX { + anyhow::bail!( + "Couldn't find a free port as we've reached the maximum port number. Consider retrying with a lower base port." + ); + } + + match TcpListener::bind(addr).await { + Ok(listener) => { + found_listener = Some(listener); + break; + } + Err(err) if err.kind() == ErrorKind::AddrInUse => { + addr.set_port(addr.port() + 1); + continue; + } + Err(err) => anyhow::bail!("Unable to listen on {addr}: {err:?}",), + } + } + + found_listener.ok_or_else(|| anyhow::anyhow!( + "Couldn't find a free port in the range {}-{}. Consider retrying with a different base port.", + self.listen_addr.port(), + self.listen_addr.port() + MAX_RETRIES + )) + } + async fn serve_http(self: Arc, listener: TcpListener) -> anyhow::Result<()> { self.print_startup_msgs("http", &listener)?; loop { diff --git a/tests/testing-framework/src/runtimes/in_process_spin.rs b/tests/testing-framework/src/runtimes/in_process_spin.rs index b9320e68c7..5387ecbdf7 100644 --- a/tests/testing-framework/src/runtimes/in_process_spin.rs +++ b/tests/testing-framework/src/runtimes/in_process_spin.rs @@ -104,7 +104,7 @@ async fn initialize_trigger( .await?; let app = spin_app::App::new("my-app", locked_app); - let trigger = HttpTrigger::new(&app, "127.0.0.1:80".parse().unwrap(), None)?; + let trigger = HttpTrigger::new(&app, "127.0.0.1:80".parse().unwrap(), None, false)?; let mut builder = TriggerAppBuilder::<_, FactorsBuilder>::new(trigger); let trigger_app = builder .build(