From 121ac66f7df66ebb1ce254eff14ce779f90404f9 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sun, 11 Oct 2026 13:53:52 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20--rm=20=E6=94=AF=E6=8C=81=20TCP=20?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E7=AB=AF=E5=8F=A3=EF=BC=8C=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=200=20=E7=AB=AF=E5=8F=A3=E5=90=8E=E5=89=8D=E5=8F=B0=E6=89=93?= =?UTF-8?q?=E5=8D=B0=E5=AE=9E=E9=99=85=E7=AB=AF=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 40 +++++------ Cargo.toml | 2 +- crates/oxide_cli/src/main.rs | 16 +++-- crates/oxide_cli/src/server/server.rs | 98 ++++++++++++++++++++++++++- crates/oxide_net/src/transport.rs | 40 +++++++++++ 5 files changed, 168 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e001a94b..56db31ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,7 +1160,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.15.7" +version = "0.15.8" dependencies = [ "chrono", "fast-float", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.15.7" +version = "0.15.8" dependencies = [ "num-bigint", "oxide_log", @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.15.7" +version = "0.15.8" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1204,7 +1204,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.15.7" +version = "0.15.8" dependencies = [ "ansi_term", "bumpalo", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.15.7" +version = "0.15.8" dependencies = [ "lru", "oxide_bytecode", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.15.7" +version = "0.15.8" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1257,7 +1257,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.15.7" +version = "0.15.8" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1271,7 +1271,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.15.7" +version = "0.15.8" dependencies = [ "num-bigint", "oxc_allocator", @@ -1286,7 +1286,7 @@ dependencies = [ [[package]] name = "oxide_engine" -version = "0.15.7" +version = "0.15.8" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1298,7 +1298,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.15.7" +version = "0.15.8" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1307,7 +1307,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.15.7" +version = "0.15.8" dependencies = [ "bumpalo", "dashmap", @@ -1321,7 +1321,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.15.7" +version = "0.15.8" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1333,7 +1333,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.15.7" +version = "0.15.8" dependencies = [ "tracing", "tracing-appender", @@ -1342,11 +1342,11 @@ dependencies = [ [[package]] name = "oxide_net" -version = "0.15.7" +version = "0.15.8" [[package]] name = "oxide_parser" -version = "0.15.7" +version = "0.15.8" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1361,7 +1361,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.15.7" +version = "0.15.8" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1377,7 +1377,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.15.7" +version = "0.15.8" dependencies = [ "num-bigint", "num-traits", @@ -1390,7 +1390,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.15.7" +version = "0.15.8" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1408,7 +1408,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.15.7" +version = "0.15.8" dependencies = [ "bumpalo", "dashmap", @@ -1421,7 +1421,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.15.7" +version = "0.15.8" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index fa181a75..896a791e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ members = [ ] [workspace.package] -version = "0.15.7" +version = "0.15.8" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_cli/src/main.rs b/crates/oxide_cli/src/main.rs index e2daef49..1223a3e5 100644 --- a/crates/oxide_cli/src/main.rs +++ b/crates/oxide_cli/src/main.rs @@ -125,9 +125,10 @@ enum ServerCommands { /// 常驻 worker 线程数;缺省取宿主核数。 #[arg(long)] workers: Option, - /// TCP 监听端口;缺省取 well-known 端口。 - #[arg(long, default_value_t = sidecar::WELL_KNOWN_PORT, conflicts_with = "rm")] - port: u16, + /// TCP 监听端口;0 表示动态端口(操作系统分配空闲端口),缺省取 + /// well-known 端口。 + #[arg(long)] + port: Option, /// 前台形态:当前进程直接运行持久 server(守护形态 spawn 的子进程用, /// 不向用户暴露)。 #[arg(long, hide = true)] @@ -340,8 +341,15 @@ fn server_command(command: ServerCommands) -> ExitCode { if let Some(n) = workers { config.worker_count = n as usize; } + // 显式端口映射 TCP 端点(回环地址硬编码):0 为动态端口, + // 由操作系统在绑定后分配空闲端口;不带端口旗标时保持 Unix + // 缺省。 + if let Some(port) = port { + config.endpoint = Endpoint::Tcp(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port)); + } start_result(run_server_rm(&config)) } else if foreground { + let port = port.unwrap_or(sidecar::WELL_KNOWN_PORT); let mut config = ServerConfig { endpoint: Endpoint::Tcp(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port)), ..ServerConfig::well_known() @@ -351,7 +359,7 @@ fn server_command(command: ServerCommands) -> ExitCode { } start_result(run_server(&config)) } else { - start_daemon(workers, port) + start_daemon(workers, port.unwrap_or(sidecar::WELL_KNOWN_PORT)) } } ServerCommands::Cleanup => match well_known_cleanup() { diff --git a/crates/oxide_cli/src/server/server.rs b/crates/oxide_cli/src/server/server.rs index dd65970f..e45a6937 100644 --- a/crates/oxide_cli/src/server/server.rs +++ b/crates/oxide_cli/src/server/server.rs @@ -272,7 +272,9 @@ pub fn run_server(config: &ServerConfig) -> Result<(), ServerError> { /// /// # 步骤 /// 1. 建关闭标志并注册信号处理器(幂等);失败直接返回(无 sidecar 可回滚)。 -/// 2. 绑定监听器(进程唯一路径,与持久路径共用同一绑定函数)。 +/// 2. 绑定监听器(进程唯一路径,与持久路径共用同一绑定函数);TCP 臂取实际 +/// 本地地址、向前台标准输出打印端点(就绪信号的一部分),上下文端点文本 +/// 取实际地址。 /// 3. 建内核(标准配置)、共享聚合计数器,派生 worker(参数与持久 server 一致)。 /// 4. 空闲窗口一:等首个连接;空闲超时内无连接即干净退出(正常退出,非错误)。 /// 5. 首个连接派生连接线程:处理完毕(EOF 或读超时)后置关闭标志并减在途计数。 @@ -289,6 +291,7 @@ pub fn run_server(config: &ServerConfig) -> Result<(), ServerError> { /// /// # 副作用 /// - 创建 socket 文件(Unix 臂),正常退出时删除;不写 sidecar。 +/// - TCP 臂向前台标准输出打印实际监听端点(host:port)。 /// - 注册进程级信号处理器(SIGINT、SIGTERM,termination 特性下含 SIGHUP)。 /// - 派生 N 个常驻 worker 线程与每连接一个 I/O 线程。 /// @@ -307,6 +310,18 @@ pub fn run_server_rm(config: &RmServerConfig) -> Result<(), ServerError> { // 绑定函数(删除重试逻辑保留只为共用)。 let listener = bind_listener_endpoint(&config.endpoint)?; + // TCP 臂:取操作系统分配的实际端口(绑 0 端口时由内核分配),向前台 + // 标准输出打印实际端点(打印在空闲窗口之前,是就绪信号的一部分), + // 上下文端点文本取实际地址(Info 响应不暴露未绑定的 0 端口)。 + let socket_path = match &listener { + Listener::Tcp(_) => { + let text = listener.local_addr().map_err(|e| ServerError::BindFailed(e.to_string()))?; + println!("rm server 监听:{text}"); + text + } + Listener::Unix(_) => config.endpoint.describe(), + }; + // 建内核(标准配置)与共享聚合计数器(全体 worker 池的增减累加到同一聚合)。 let kernel = KernelCore::new(KernelConfig::standard()); let counters = PoolCounters::shared(); @@ -327,7 +342,7 @@ pub fn run_server_rm(config: &RmServerConfig) -> Result<(), ServerError> { shutdown: Arc::clone(&shutdown), in_flight: Arc::new(AtomicUsize::new(0)), pool_counters: counters, - socket_path: config.endpoint.describe(), + socket_path, }); // 空闲窗口一:等首个连接;空闲超时内无连接即干净退出。 @@ -809,7 +824,7 @@ fn wait_in_flight_drained(in_flight: &AtomicUsize, timeout: Duration) { mod tests { use super::*; use std::io::Write; - use std::net::TcpStream; + use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream}; use std::thread::JoinHandle; /// 测试全部用 Unix 端点;直接取 socket 路径。 @@ -892,6 +907,49 @@ mod tests { handle } + /// 预取空闲端口:临时 `TcpListener` 绑 0 端口取操作系统分配的端口后释放。 + fn free_tcp_port() -> u16 { + let listener = TcpListener::bind("127.0.0.1:0").expect("临时监听者绑定应成功"); + let port = listener.local_addr().expect("取端口应成功").port(); + drop(listener); + port + } + + /// --rm TCP 配置:固定空闲端口、worker 数 1、300 毫秒空闲超时(与 Unix + /// 配置同口径,不依赖真实超时值,断言带 5 秒裕量)。 + fn rm_tcp_config() -> RmServerConfig { + RmServerConfig { + endpoint: Endpoint::Tcp(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), free_tcp_port())), + worker_count: 1, + idle_timeout: Duration::from_millis(300), + } + } + + /// 后台线程起真实 run_server_rm(TCP 臂)。 + /// + /// TCP 无 socket 文件可等,就绪由调用方轮询 `TcpStream::connect` 判定; + /// 首个成功连接即 server 服务的单连接(监听器随后被丢弃),调用方须保留 + /// 该连接、不得探后即弃。 + fn start_rm_server_tcp(config: RmServerConfig) -> JoinHandle> { + let server_config = config.clone(); + std::thread::spawn(move || run_server_rm(&server_config)) + } + + /// 连接 TCP rm server:轮询 connect 至成功(首个成功连接即 server 服务的 + /// 单连接,返回后由调用方保留使用)。 + fn connect_rm_tcp(addr: SocketAddr) -> TcpStream { + let deadline = Instant::now() + Duration::from_secs(10); + loop { + match TcpStream::connect(addr) { + Ok(stream) => return stream, + Err(_) => { + assert!(Instant::now() < deadline, "TCP rm server 应在 10 秒内就绪"); + std::thread::sleep(Duration::from_millis(10)); + } + } + } + } + /// 信号处理器体:置位关闭标志。 #[test] fn signal_shutdown_sets_flag() { @@ -1489,4 +1547,38 @@ mod tests { stop_server(&mut stream, handle); assert!(!unix_path(&config.endpoint).exists(), "socket 文件应被删除"); } + + /// --rm TCP 臂:eval 得完成值后客户端断开,server 在有界时间内正常退出 + /// (走绑定、打印、空闲窗口、单连接的完整 TCP 代码路径)。 + #[test] + fn rm_tcp_eval_then_eof_exits() { + let config = rm_tcp_config(); + let handle = start_rm_server_tcp(config.clone()); + let addr = match &config.endpoint { + Endpoint::Tcp(addr) => *addr, + _ => panic!("应为 TCP 端点"), + }; + let mut stream = Conn::Tcp(connect_rm_tcp(addr)); + let response = send_and_recv( + &mut stream, + &ServerRequest::Eval { + code: "1 + 1".into(), + max_steps: None, + }, + ); + assert!( + matches!(response, ServerResponse::EvalResult { ref value, .. } if value.as_deref() == Some("2")), + "应得完成值为 2 的 EvalResult 帧:{response:?}" + ); + + // 客户端断开:连接线程读到 EOF 退出,server 应在空闲超时加裕量内正常退出。 + drop(stream); + let deadline = Instant::now() + config.idle_timeout + Duration::from_secs(5); + while !handle.is_finished() { + assert!(Instant::now() < deadline, "rm server 应在空闲超时加裕量内退出"); + std::thread::sleep(Duration::from_millis(10)); + } + let result = handle.join().expect("rm server 线程应正常退出"); + assert!(result.is_ok(), "rm server 应正常退出:{result:?}"); + } } diff --git a/crates/oxide_net/src/transport.rs b/crates/oxide_net/src/transport.rs index 6405b67e..8e7704ea 100644 --- a/crates/oxide_net/src/transport.rs +++ b/crates/oxide_net/src/transport.rs @@ -93,6 +93,22 @@ impl Listener { Listener::Tcp(l) => Ok(Conn::Tcp(l.accept()?.0)), } } + + /// 取监听者绑定的本地地址文本形态。 + /// + /// # 边界与前提 + /// - 两臂委托标准库同名方法后取文本形态(Unix 臂为套接字路径,TCP 臂为 + /// host:port);TCP 臂绑 0 端口时返回操作系统分配的实际端口。 + pub fn local_addr(&self) -> io::Result { + match self { + Listener::Unix(l) => { + let addr = l.local_addr()?; + let text = addr.as_pathname().map(|p| p.to_string_lossy().into_owned()).unwrap_or_default(); + Ok(text) + } + Listener::Tcp(l) => Ok(l.local_addr()?.to_string()), + } + } } impl Conn { @@ -223,6 +239,30 @@ mod tests { assert_eq!(&buf, b"hello-oxide"); } + /// 动态端口:TCP 臂绑 0 端口后 `local_addr` 文本形态含操作系统分配的非 0 端口。 + #[test] + fn tcp_local_addr_dynamic_port() { + let listener = Listener::Tcp(TcpListener::bind("127.0.0.1:0").unwrap()); + let text = listener.local_addr().unwrap(); + let port: u16 = text.rsplit(':').next().expect("应含端口").parse().expect("端口应可解析"); + assert_ne!(port, 0, "操作系统应分配非 0 端口:{text}"); + } + + /// Unix 臂 `local_addr` 返回套接字路径的文本形态(非空)。 + #[test] + fn unix_local_addr() { + let dir = std::env::temp_dir().join(format!("oxide_net_{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("unix_addr.sock"); + let _ = std::fs::remove_file(&path); + + let listener = Listener::Unix(UnixListener::bind(&path).unwrap()); + let text = listener.local_addr().unwrap(); + assert!(!text.is_empty(), "Unix 臂应得非空路径文本:{text}"); + + let _ = std::fs::remove_file(&path); + } + /// 非阻塞 accept 空轮:置非阻塞后无连接时 accept 应返回 `WouldBlock`(两臂各验一次)。 #[test] fn nonblocking_accept_would_block() {