fix(sidecar): use ephemeral ports for tcp/quic listen too

mycelium defaults --tcp-listen-port and --quic-listen-port to 9651
when not provided. If anything else holds 9651 (a previous test
instance, a Docker container from the level-1 procedure, another
mycelium running on the host), the daemon comes up far enough to
serve the loopback API for a few seconds before tearing itself
down on the listen failure.

Pick three distinct ephemeral ports (api, tcp, quic) per spawn.
Trade-off: inbound peers need the actual port number, which we
already log; the user can pin via a future SidecarConfig field.
This commit is contained in:
syoul
2026-04-25 23:51:35 +02:00
parent 4dd278e62a
commit 2cd14f06ae

View File

@@ -102,8 +102,14 @@ impl SidecarHandle {
} }
let bin = locate_sidecar(app)?; let bin = locate_sidecar(app)?;
let port = portpicker::pick_unused_port() // Three ports: HTTP API (loopback), TCP listen, QUIC (UDP) listen.
.ok_or_else(|| AppError::Other("no free port available".into()))?; // mycelium defaults to 9651 for both peer-listen ports, which
// collides if another instance (or a leftover from a previous test)
// is already up. Always picking ephemeral ports avoids that at the
// cost of inbound peers needing the actual port number.
let api_port = pick_port()?;
let tcp_port = pick_port_skip(&[api_port])?;
let quic_port = pick_port_skip(&[api_port, tcp_port])?;
let data_dir = app let data_dir = app
.path() .path()
@@ -115,7 +121,11 @@ impl SidecarHandle {
let mut args = vec![ let mut args = vec![
"--api-addr".to_string(), "--api-addr".to_string(),
format!("127.0.0.1:{port}"), format!("127.0.0.1:{api_port}"),
"--tcp-listen-port".to_string(),
tcp_port.to_string(),
"--quic-listen-port".to_string(),
quic_port.to_string(),
"--key-file".to_string(), "--key-file".to_string(),
key_path.display().to_string(), key_path.display().to_string(),
]; ];
@@ -137,7 +147,11 @@ impl SidecarHandle {
} }
} }
info!(?bin, port, "spawning mycelium sidecar via pkexec"); info!(
?bin,
api_port, tcp_port, quic_port,
"spawning mycelium sidecar via pkexec"
);
let mut cmd = elevation::elevated(&bin, &args); let mut cmd = elevation::elevated(&bin, &args);
cmd.stdout(Stdio::piped()) cmd.stdout(Stdio::piped())
@@ -150,7 +164,7 @@ impl SidecarHandle {
// Stash before we await the health check, so a slow daemon // Stash before we await the health check, so a slow daemon
// doesn't leave us with a zombie process if anything panics. // doesn't leave us with a zombie process if anything panics.
let api_url = format!("http://127.0.0.1:{port}"); let api_url = format!("http://127.0.0.1:{api_port}");
*self.child.lock() = Some(child); *self.child.lock() = Some(child);
*self.api_url.lock() = Some(api_url.clone()); *self.api_url.lock() = Some(api_url.clone());
*self.config_path.lock() = Some(config_path); *self.config_path.lock() = Some(config_path);
@@ -257,6 +271,22 @@ impl SidecarHandle {
} }
} }
fn pick_port() -> AppResult<u16> {
portpicker::pick_unused_port().ok_or_else(|| AppError::Other("no free port available".into()))
}
fn pick_port_skip(taken: &[u16]) -> AppResult<u16> {
for _ in 0..16 {
let p = pick_port()?;
if !taken.contains(&p) {
return Ok(p);
}
}
Err(AppError::Other(
"could not find a unique free port".into(),
))
}
/// Resolve the bundled `mycelium-<triple>` binary in both `tauri dev` /// Resolve the bundled `mycelium-<triple>` binary in both `tauri dev`
/// (cargo manifest) and bundled (resource_dir) modes. /// (cargo manifest) and bundled (resource_dir) modes.
fn locate_sidecar(app: &AppHandle) -> AppResult<PathBuf> { fn locate_sidecar(app: &AppHandle) -> AppResult<PathBuf> {