mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
feat: server 启动加 --host 绑定地址旗标与 sidecar 端口字段
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::fs;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::process::ExitCode;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
@@ -129,6 +129,9 @@ enum ServerCommands {
|
||||
/// well-known 端口。
|
||||
#[arg(long)]
|
||||
port: Option<u16>,
|
||||
/// TCP 绑定地址;缺省 loopback(127.0.0.1),两臂(--rm 与持久)共享。
|
||||
#[arg(long, default_value = "127.0.0.1")]
|
||||
host: IpAddr,
|
||||
/// 前台形态:当前进程直接运行持久 server(守护形态 spawn 的子进程用,
|
||||
/// 不向用户暴露)。
|
||||
#[arg(long, hide = true)]
|
||||
@@ -331,6 +334,7 @@ fn server_command(command: ServerCommands) -> ExitCode {
|
||||
idle_timeout,
|
||||
workers,
|
||||
port,
|
||||
host,
|
||||
foreground,
|
||||
} => {
|
||||
if rm {
|
||||
@@ -341,17 +345,18 @@ fn server_command(command: ServerCommands) -> ExitCode {
|
||||
if let Some(n) = workers {
|
||||
config.worker_count = n as usize;
|
||||
}
|
||||
// 显式端口映射 TCP 端点(回环地址硬编码):0 为动态端口,
|
||||
// 显式端口映射 TCP 端点(绑定地址取 --host):0 为动态端口,
|
||||
// 由操作系统在绑定后分配空闲端口;不带端口旗标时保持 Unix
|
||||
// 缺省。
|
||||
if let Some(port) = port {
|
||||
config.endpoint = Endpoint::Tcp(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port));
|
||||
config.endpoint = Endpoint::Tcp(SocketAddr::new(host, 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)),
|
||||
endpoint: Endpoint::Tcp(SocketAddr::new(host, port)),
|
||||
bind_addr: host,
|
||||
..ServerConfig::well_known()
|
||||
};
|
||||
if let Some(n) = workers {
|
||||
@@ -359,7 +364,7 @@ fn server_command(command: ServerCommands) -> ExitCode {
|
||||
}
|
||||
start_result(run_server(&config))
|
||||
} else {
|
||||
start_daemon(workers, port.unwrap_or(sidecar::WELL_KNOWN_PORT))
|
||||
start_daemon(workers, host, port.unwrap_or(sidecar::WELL_KNOWN_PORT))
|
||||
}
|
||||
}
|
||||
ServerCommands::Cleanup => match well_known_cleanup() {
|
||||
@@ -491,7 +496,7 @@ fn control_arm(request: &ServerRequest, render: fn(&ServerResponse) -> Option<St
|
||||
}
|
||||
|
||||
/// start 持久形态:spawn 脱离的前台 server 子进程(`server start
|
||||
/// --foreground`),就绪探测通过后打印返回。
|
||||
/// --foreground --host H --port N`),就绪探测通过后打印返回。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. spawn 脱离的前台 server 子进程(标准流全空、不等待);spawn 失败以
|
||||
@@ -508,9 +513,9 @@ fn control_arm(request: &ServerRequest, render: fn(&ServerResponse) -> Option<St
|
||||
///
|
||||
/// # 副作用
|
||||
/// - server 为脱离进程,CLI 退出后由 init 收领继续运行。
|
||||
fn start_daemon(workers: Option<u32>, port: u16) -> ExitCode {
|
||||
fn start_daemon(workers: Option<u32>, host: IpAddr, port: u16) -> ExitCode {
|
||||
// spawn:脱离子进程,标准流全空,不等待。
|
||||
if let Err(err) = spawn::spawn_detached_server(workers, port) {
|
||||
if let Err(err) = spawn::spawn_detached_server(workers, host, port) {
|
||||
eprintln!("{}", Red.paint(format!("spawn server 进程失败:{err}")));
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
@@ -563,8 +568,8 @@ fn restart_command() -> ExitCode {
|
||||
}
|
||||
|
||||
// 拉起新 server:脱离进程,标准流全空,不等待。新 server 按缺省配置拉起
|
||||
// (well-known 端口,不继承旧旗标)。
|
||||
if let Err(err) = spawn::spawn_detached_server(None, sidecar::WELL_KNOWN_PORT) {
|
||||
// (well-known 绑定地址与端口,不继承旧旗标)。
|
||||
if let Err(err) = spawn::spawn_detached_server(None, sidecar::WELL_KNOWN_BIND_ADDR, sidecar::WELL_KNOWN_PORT) {
|
||||
eprintln!("{}", Red.paint(format!("拉起新 server 失败:{err}")));
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
@@ -855,3 +860,41 @@ fn not_implemented(command: &str) -> ExitCode {
|
||||
// 2 = 未实现,与 0=成功、1=运行失败区分,供脚本与 CI 识别。
|
||||
ExitCode::from(2)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
|
||||
/// 取解析结果的 start 臂 host 字段(两臂共享的 --host 旗标)。
|
||||
fn parsed_host(args: &[&str]) -> IpAddr {
|
||||
let cli = Cli::try_parse_from(args).expect("解析应成功");
|
||||
let Some(Commands::Server { command }) = cli.command else {
|
||||
panic!("应解析出 server 臂");
|
||||
};
|
||||
let ServerCommands::Start { host, .. } = command else {
|
||||
panic!("应解析出 start 臂");
|
||||
};
|
||||
host
|
||||
}
|
||||
|
||||
/// --host 缺省取 loopback:不带旗标解析得 127.0.0.1,显式旗标原样解析,
|
||||
/// 非法取值走 clap 错误。
|
||||
#[test]
|
||||
fn start_host_flag_parses() {
|
||||
assert_eq!(
|
||||
parsed_host(&["oxide", "server", "start"]),
|
||||
IpAddr::V4(Ipv4Addr::LOCALHOST),
|
||||
"缺省 host 应为 loopback"
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_host(&["oxide", "server", "start", "--host", "0.0.0.0"]),
|
||||
"0.0.0.0".parse::<IpAddr>().unwrap(),
|
||||
"显式 host 应原样解析"
|
||||
);
|
||||
assert!(
|
||||
Cli::try_parse_from(["oxide", "server", "start", "--host", "not-an-ip"]).is_err(),
|
||||
"非法 host 应走 clap 错误"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,7 +211,10 @@ pub fn cleanup_with_wait(sidecar_path: &Path, endpoint: &Endpoint, kill_wait: Du
|
||||
/// # 注意事项
|
||||
/// - CLI 分派臂与启动 liveness 扫描的稳定入口。
|
||||
pub fn well_known_cleanup() -> CleanupOutcome {
|
||||
cleanup(&sidecar::well_known_sidecar_path(), &sidecar::well_known_endpoint())
|
||||
cleanup(
|
||||
&sidecar::well_known_sidecar_path(),
|
||||
&sidecar::well_known_endpoint(sidecar::WELL_KNOWN_BIND_ADDR),
|
||||
)
|
||||
}
|
||||
|
||||
/// 发送 SIGTERM:`kill -TERM`,与 `pid_alive` 的 `kill -0` 同一 `Command` 路径。
|
||||
@@ -302,6 +305,7 @@ mod tests {
|
||||
version: "0.0.0".into(),
|
||||
started_at: 0,
|
||||
starttime,
|
||||
port: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -258,6 +258,7 @@ mod tests {
|
||||
version: "0.0.0".into(),
|
||||
started_at: 0,
|
||||
starttime,
|
||||
port: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
use std::net::{IpAddr, TcpListener, TcpStream};
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
@@ -86,17 +86,21 @@ pub struct ServerConfig {
|
||||
pub worker_count: usize,
|
||||
/// 本进程构建版本,版本比对与 sidecar 写入用;生产默认取构建期版本。
|
||||
pub version: String,
|
||||
/// TCP 绑定地址(loopback 缺省);端点由 CLI 层按本字段加端口构造,
|
||||
/// Unix 臂不受影响。
|
||||
pub bind_addr: IpAddr,
|
||||
}
|
||||
|
||||
impl ServerConfig {
|
||||
/// 生产默认配置:endpoint 取 well-known TCP 端口,sidecar 取 well-known
|
||||
/// 位置,worker 数取宿主核数,版本取构建期版本。
|
||||
/// 生产默认配置:endpoint 取 well-known TCP 端口(loopback 绑定地址),
|
||||
/// sidecar 取 well-known 位置,worker 数取宿主核数,版本取构建期版本。
|
||||
pub fn well_known() -> Self {
|
||||
ServerConfig {
|
||||
endpoint: sidecar::well_known_endpoint(),
|
||||
endpoint: sidecar::well_known_endpoint(sidecar::WELL_KNOWN_BIND_ADDR),
|
||||
sidecar_path: sidecar::well_known_sidecar_path(),
|
||||
worker_count: default_worker_count(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
bind_addr: sidecar::WELL_KNOWN_BIND_ADDR,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -842,6 +846,7 @@ mod tests {
|
||||
sidecar_path: dir.join("sidecar.json"),
|
||||
worker_count: 2,
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
bind_addr: IpAddr::V4(Ipv4Addr::LOCALHOST),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1419,6 +1424,7 @@ mod tests {
|
||||
.join(format!("oxide_server_tcp_yield_{}_sidecar.json", std::process::id())),
|
||||
worker_count: 2,
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
bind_addr: IpAddr::V4(Ipv4Addr::LOCALHOST),
|
||||
};
|
||||
yield_and_takeover(&config).expect("TCP 臂交接应成功");
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ use serde::{Deserialize, Serialize};
|
||||
/// - `started_at`:Unix 秒,人读展示用。
|
||||
/// - `starttime`:`/proc/<pid>/stat` 的启动时刻刻度(自开机单调递增),
|
||||
/// PID 复用判定用。
|
||||
/// - `port`:TCP 监听端口(机器可读形态),Unix 臂为 0;旧 sidecar 无该
|
||||
/// 字段时读回为 0(serde 缺省),探活裁决仍走 `socket_path` 文本。
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ServerIdentity {
|
||||
pub pid: u32,
|
||||
@@ -40,27 +42,37 @@ pub struct ServerIdentity {
|
||||
pub version: String,
|
||||
pub started_at: u64,
|
||||
pub starttime: u64,
|
||||
/// TCP 监听端口(机器可读形态);Unix 臂为 0。旧 sidecar 无该字段时
|
||||
/// 读回为 0(serde 缺省),探活裁决走 socket_path 文本,行为不变。
|
||||
#[serde(default)]
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
impl ServerIdentity {
|
||||
/// 构造当前进程的身份(pid、版本、启动时间、启动时刻刻度)。
|
||||
/// 构造当前进程的身份(pid、版本、启动时间、启动时刻刻度、监听端口)。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 版本由调用方注入(生产默认取构建期版本,测试注入可区分值),
|
||||
/// 本函数不读取构建期版本。
|
||||
/// - `socket_path` 字段记监听端点的文本形态(`endpoint.describe()`),
|
||||
/// 字段名与 JSON 模式不变,旧 sidecar 可读。
|
||||
/// - `port` 取端点的 TCP 端口(Unix 臂无端口,记 0)。
|
||||
/// - 非 Linux 宿主(`/proc` 缺位)时 `starttime` 填 0,交叉核对退化,
|
||||
/// liveness 由 endpoint 探活裁决。
|
||||
pub fn new(endpoint: &Endpoint, version: &str) -> Self {
|
||||
let pid = std::process::id();
|
||||
let started_at = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
|
||||
let port = match endpoint {
|
||||
Endpoint::Tcp(addr) => addr.port(),
|
||||
Endpoint::Unix(_) => 0,
|
||||
};
|
||||
ServerIdentity {
|
||||
pid,
|
||||
socket_path: endpoint.describe(),
|
||||
version: version.to_string(),
|
||||
started_at,
|
||||
starttime: process_starttime(pid).unwrap_or(0),
|
||||
port,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,6 +99,9 @@ pub fn current_uid() -> u32 {
|
||||
/// well-known TCP 端口:持久 server 缺省端口。
|
||||
pub const WELL_KNOWN_PORT: u16 = 4242;
|
||||
|
||||
/// well-known TCP 绑定地址:缺省 loopback(持久 server 缺省只接受本机连接)。
|
||||
pub const WELL_KNOWN_BIND_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST);
|
||||
|
||||
/// well-known sidecar 路径:`$TMPDIR/oxide-<uid>.json`。
|
||||
pub fn well_known_sidecar_path() -> PathBuf {
|
||||
std::env::temp_dir().join(format!("oxide-{}.json", current_uid()))
|
||||
@@ -100,9 +115,9 @@ pub fn well_known_socket_path() -> PathBuf {
|
||||
std::env::temp_dir().join(format!("oxide-{}.sock", current_uid()))
|
||||
}
|
||||
|
||||
/// well-known TCP 端点:127.0.0.1:WELL_KNOWN_PORT(绑定地址固定 loopback)。
|
||||
pub fn well_known_endpoint() -> Endpoint {
|
||||
Endpoint::Tcp(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), WELL_KNOWN_PORT))
|
||||
/// well-known TCP 端点:bind_addr:WELL_KNOWN_PORT(缺省取 loopback)。
|
||||
pub fn well_known_endpoint(bind_addr: IpAddr) -> Endpoint {
|
||||
Endpoint::Tcp(SocketAddr::new(bind_addr, WELL_KNOWN_PORT))
|
||||
}
|
||||
|
||||
/// 解析端点文本形态:先试 `SocketAddr`(host:port)得 TCP 臂,失败回
|
||||
@@ -408,6 +423,7 @@ mod tests {
|
||||
version: "0.0.0".into(),
|
||||
started_at: 0,
|
||||
starttime: 0,
|
||||
port: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,6 +438,42 @@ mod tests {
|
||||
assert_eq!(read, id, "五字段应逐字段一致");
|
||||
}
|
||||
|
||||
/// 端口字段:TCP 端点记端点端口(机器可读形态),Unix 臂记 0。
|
||||
#[test]
|
||||
fn identity_port_per_arm() {
|
||||
let tcp = ServerIdentity::new(&Endpoint::Tcp(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 4242)), "0.0.0");
|
||||
assert_eq!(tcp.port, 4242, "TCP 臂应记端点端口");
|
||||
let unix = ServerIdentity::new(&Endpoint::Unix(PathBuf::from("/tmp/oxide-test.sock")), "0.0.0");
|
||||
assert_eq!(unix.port, 0, "Unix 臂应记 0");
|
||||
}
|
||||
|
||||
/// 写读回环(TCP 臂):sidecar 含机器可读 port 字段,读回逐字段一致。
|
||||
#[test]
|
||||
fn identity_roundtrip_records_tcp_port() {
|
||||
let dir = TestDir::new();
|
||||
let path = dir.path("sidecar.json");
|
||||
let endpoint = Endpoint::Tcp(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 4242));
|
||||
let id = ServerIdentity::new(&endpoint, "0.0.0");
|
||||
write_exclusive(&id, &path).expect("写 sidecar 应成功");
|
||||
let read = read_identity(&path).expect("读回 sidecar 应成功");
|
||||
assert_eq!(read, id, "字段应逐字段一致");
|
||||
assert_eq!(read.port, 4242, "port 字段应记端点端口");
|
||||
}
|
||||
|
||||
/// 旧 sidecar(无 port 字段)读回:serde 缺省补 0,读回成功不判损坏。
|
||||
#[test]
|
||||
fn legacy_sidecar_without_port_reads_back() {
|
||||
let dir = TestDir::new();
|
||||
let path = dir.path("sidecar.json");
|
||||
fs::write(
|
||||
&path,
|
||||
r#"{"pid":1,"socket_path":"127.0.0.1:4242","version":"0.0.0","started_at":0,"starttime":0}"#,
|
||||
)
|
||||
.expect("写旧 sidecar 应成功");
|
||||
let id = read_identity(&path).expect("旧 sidecar 应可读回");
|
||||
assert_eq!(id.port, 0, "无 port 字段应读回 0");
|
||||
}
|
||||
|
||||
/// O_EXCL 排他:同路径二次 create_new 必得 AlreadyExists。
|
||||
#[test]
|
||||
fn write_exclusive_second_create_fails() {
|
||||
|
||||
@@ -4,31 +4,41 @@
|
||||
//! spawn 逻辑。`current_exe()` 在 lib 上下文与二进制目标返回同一路径(lib
|
||||
//! 链接进同一个 oxide 二进制),spawn 行为逐字节不变。
|
||||
|
||||
use std::net::IpAddr;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::client::send_control_request;
|
||||
use super::protocol::{ServerRequest, ServerResponse};
|
||||
|
||||
/// spawn 脱离的前台 server 子进程(`server start --foreground --port N`):
|
||||
/// spawn 脱离的前台 server 子进程(`server start --foreground --host H --port N`):
|
||||
/// 标准流全空,不等待。
|
||||
///
|
||||
/// # 步骤
|
||||
/// 1. 取当前可执行文件路径。
|
||||
/// 2. 构造命令 `server start --foreground --port N` 加可选 `--workers N`。
|
||||
/// 2. 构造命令 `server start --foreground --host H --port N` 加可选 `--workers N`。
|
||||
/// 3. 三个标准流置空并 spawn。
|
||||
///
|
||||
/// # 边界与前提
|
||||
/// - 必须带 `--foreground`:守护形态 spawn 的是前台 server 入口,不带该旗标会
|
||||
/// 再次进入守护形态形成递归 spawn。
|
||||
/// - `port` 是 TCP 监听端口;缺省取 well-known 端口(调用方传入)。
|
||||
/// - `host` 是 TCP 绑定地址(调用方传入,缺省 loopback);`port` 是 TCP 监听
|
||||
/// 端口,缺省取 well-known 端口(调用方传入)。
|
||||
///
|
||||
/// # 副作用
|
||||
/// - 创建脱离的子进程,调用方退出后由 init 收领继续运行。
|
||||
pub fn spawn_detached_server(workers: Option<u32>, port: u16) -> std::io::Result<std::process::Child> {
|
||||
pub fn spawn_detached_server(workers: Option<u32>, host: IpAddr, port: u16) -> std::io::Result<std::process::Child> {
|
||||
let exe = std::env::current_exe()?;
|
||||
let mut cmd = Command::new(exe);
|
||||
cmd.args(["server", "start", "--foreground", "--port", &port.to_string()]);
|
||||
cmd.args([
|
||||
"server",
|
||||
"start",
|
||||
"--foreground",
|
||||
"--host",
|
||||
&host.to_string(),
|
||||
"--port",
|
||||
&port.to_string(),
|
||||
]);
|
||||
if let Some(n) = workers {
|
||||
cmd.args(["--workers", &n.to_string()]);
|
||||
}
|
||||
|
||||
@@ -159,8 +159,8 @@ pub fn run_watchdog(workers: Option<u32>) -> ExitCode {
|
||||
}
|
||||
|
||||
// 初始 spawn 与就绪探测:失败即退 1,不进入监控循环。watchdog 子进程用
|
||||
// well-known 端口(范围控制:不加 --port 旗标)。
|
||||
let mut child = match spawn_detached_server(workers, sidecar::WELL_KNOWN_PORT) {
|
||||
// well-known 绑定地址与端口(范围控制:不加 --host / --port 旗标)。
|
||||
let mut child = match spawn_detached_server(workers, sidecar::WELL_KNOWN_BIND_ADDR, sidecar::WELL_KNOWN_PORT) {
|
||||
Ok(child) => child,
|
||||
Err(err) => {
|
||||
eprintln!("spawn server 进程失败:{err}");
|
||||
@@ -246,7 +246,7 @@ pub fn run_watchdog(workers: Option<u32>) -> ExitCode {
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
std::thread::sleep(CRASH_BACKOFF);
|
||||
match spawn_detached_server(workers, sidecar::WELL_KNOWN_PORT) {
|
||||
match spawn_detached_server(workers, sidecar::WELL_KNOWN_BIND_ADDR, sidecar::WELL_KNOWN_PORT) {
|
||||
Ok(new_child) => {
|
||||
child = new_child;
|
||||
child_exited = None;
|
||||
@@ -308,6 +308,7 @@ mod tests {
|
||||
version: "0.0.0".into(),
|
||||
started_at: 0,
|
||||
starttime: 0,
|
||||
port: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user