Skip to content

Commit cb47654

Browse files
authored
Merge pull request #3182 from seun-ja/opt-in-custom-port
adding custom port in cli opt-in
2 parents 36a4fcc + dfbef34 commit cb47654

File tree

3 files changed

+78
-10
lines changed

3 files changed

+78
-10
lines changed

crates/trigger-http/src/lib.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ pub struct CliArgs {
5050
/// 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
5151
#[clap(long, env = "SPIN_TLS_KEY", requires = "tls-cert")]
5252
pub tls_key: Option<PathBuf>,
53+
54+
#[clap(long = "find-free-port")]
55+
pub find_free_port: bool,
5356
}
5457

5558
impl CliArgs {
@@ -73,6 +76,7 @@ pub struct HttpTrigger {
7376
/// If the port is set to 0, the actual address will be determined by the OS.
7477
listen_addr: SocketAddr,
7578
tls_config: Option<TlsConfig>,
79+
find_free_port: bool,
7680
}
7781

7882
impl<F: RuntimeFactors> Trigger<F> for HttpTrigger {
@@ -82,7 +86,14 @@ impl<F: RuntimeFactors> Trigger<F> for HttpTrigger {
8286
type InstanceState = ();
8387

8488
fn new(cli_args: Self::CliArgs, app: &spin_app::App) -> anyhow::Result<Self> {
85-
Self::new(app, cli_args.address, cli_args.into_tls_config())
89+
let find_free_port = cli_args.find_free_port;
90+
91+
Self::new(
92+
app,
93+
cli_args.address,
94+
cli_args.into_tls_config(),
95+
find_free_port,
96+
)
8697
}
8798

8899
async fn run(self, trigger_app: TriggerApp<F>) -> anyhow::Result<()> {
@@ -104,12 +115,14 @@ impl HttpTrigger {
104115
app: &spin_app::App,
105116
listen_addr: SocketAddr,
106117
tls_config: Option<TlsConfig>,
118+
find_free_port: bool,
107119
) -> anyhow::Result<Self> {
108120
Self::validate_app(app)?;
109121

110122
Ok(Self {
111123
listen_addr,
112124
tls_config,
125+
find_free_port,
113126
})
114127
}
115128

@@ -121,8 +134,14 @@ impl HttpTrigger {
121134
let Self {
122135
listen_addr,
123136
tls_config,
137+
find_free_port,
124138
} = self;
125-
let server = Arc::new(HttpServer::new(listen_addr, tls_config, trigger_app)?);
139+
let server = Arc::new(HttpServer::new(
140+
listen_addr,
141+
tls_config,
142+
find_free_port,
143+
trigger_app,
144+
)?);
126145
Ok(server)
127146
}
128147

crates/trigger-http/src/server.rs

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
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

39
use anyhow::{bail, Context};
410
use 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.
4553
pub 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 {

tests/testing-framework/src/runtimes/in_process_spin.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ async fn initialize_trigger(
104104
.await?;
105105

106106
let app = spin_app::App::new("my-app", locked_app);
107-
let trigger = HttpTrigger::new(&app, "127.0.0.1:80".parse().unwrap(), None)?;
107+
let trigger = HttpTrigger::new(&app, "127.0.0.1:80".parse().unwrap(), None, false)?;
108108
let mut builder = TriggerAppBuilder::<_, FactorsBuilder>::new(trigger);
109109
let trigger_app = builder
110110
.build(

0 commit comments

Comments
 (0)