修复本地回环端口池耗尽

让Agent Runner与本地预览复用Linux非临时端口回退
让MCP HTTP测试夹具预留可用端口并有限重试
补充端口池耗尽排障与验证约束
This commit is contained in:
AIGameCreator App
2026-07-17 07:12:08 +08:00
parent 0e1b158579
commit 570d346db2
4 changed files with 50 additions and 30 deletions
@@ -289,8 +289,8 @@ pub(crate) fn start_local_game_preview_for_project(
));
}
let listener =
TcpListener::bind(("127.0.0.1", 0)).map_err(|error| format!("启动预览失败:{error}"))?;
let listener = bind_loopback_listener_with_linux_fallback(root.to_string_lossy().as_ref())
.map_err(|error| format!("启动预览失败:{error}"))?;
let port = listener
.local_addr()
.map_err(|error| format!("读取预览端口失败:{error}"))?
@@ -3009,18 +3009,18 @@ fn read_external_agent_runner_linux_fallback_ports(boot_id: &str) -> Option<Vec<
))
}
fn bind_external_agent_runner_listener(boot_id: &str) -> io::Result<TcpListener> {
pub(crate) fn bind_loopback_listener_with_linux_fallback(seed: &str) -> io::Result<TcpListener> {
#[cfg(target_os = "linux")]
{
return bind_external_agent_runner_listener_with(
|| read_external_agent_runner_linux_fallback_ports(boot_id).unwrap_or_default(),
|| read_external_agent_runner_linux_fallback_ports(seed).unwrap_or_default(),
|port| TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port)),
);
}
#[cfg(not(target_os = "linux"))]
{
let _ = boot_id;
let _ = seed;
TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0))
}
}
@@ -3038,7 +3038,7 @@ pub(crate) fn run_external_agent_runner_server(config_dir: impl AsRef<Path>) ->
&external_agent_runner_lock_path(&config_dir),
&boot_id,
)?;
let listener = bind_external_agent_runner_listener(&boot_id)
let listener = bind_loopback_listener_with_linux_fallback(&boot_id)
.map_err(|error| format!("绑定 Agent Runner loopback 端口失败:{error}"))?;
listener
.set_nonblocking(true)
@@ -2768,26 +2768,46 @@ impl Drop for McpHttpFixtureChild {
}
fn spawn_mcp_http_fixture() -> (McpHttpFixtureChild, u16) {
let mut child = std::process::Command::new("node")
.arg(mcp_fixture_script_path())
.arg("http")
.arg("0")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit())
.spawn()
.expect("spawn MCP HTTP fixture");
let stdout = child.stdout.take().expect("MCP HTTP fixture stdout");
let mut line = String::new();
BufReader::new(stdout)
.read_line(&mut line)
.expect("read MCP HTTP fixture address");
let port = serde_json::from_str::<serde_json::Value>(&line)
.expect("parse MCP HTTP fixture address")["port"]
.as_u64()
.and_then(|value| u16::try_from(value).ok())
.expect("MCP HTTP fixture port");
(McpHttpFixtureChild(child), port)
const MCP_HTTP_FIXTURE_BIND_ATTEMPTS: usize = 8;
let mut last_error = "未尝试启动".to_string();
for _ in 0..MCP_HTTP_FIXTURE_BIND_ATTEMPTS {
let reservation = bind_test_tcp_listener("reserve MCP HTTP fixture port");
let requested_port = reservation
.local_addr()
.expect("read reserved MCP HTTP fixture port")
.port();
drop(reservation);
let mut child = std::process::Command::new("node")
.arg(mcp_fixture_script_path())
.arg("http")
.arg(requested_port.to_string())
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit())
.spawn()
.expect("spawn MCP HTTP fixture");
let stdout = child.stdout.take().expect("MCP HTTP fixture stdout");
let mut line = String::new();
match BufReader::new(stdout).read_line(&mut line) {
Ok(0) => last_error = "Node 在报告 MCP HTTP 地址前退出".to_string(),
Ok(_) => {
let reported_port = serde_json::from_str::<serde_json::Value>(&line)
.ok()
.and_then(|value| value["port"].as_u64())
.and_then(|value| u16::try_from(value).ok());
if reported_port == Some(requested_port) {
return (McpHttpFixtureChild(child), requested_port);
}
last_error =
format!("Node 报告的 MCP HTTP 端口与预留端口不一致:{reported_port:?}");
}
Err(error) => last_error = format!("读取 MCP HTTP fixture 地址失败:{error}"),
}
let _ = child.kill();
let _ = child.wait();
}
panic!("启动 MCP HTTP fixture 失败:{last_error}")
}
#[tokio::test]