feat: server start 接通端口旗标与 well-known TCP 端点

This commit is contained in:
2026-10-11 13:34:45 +00:00
parent b40628fed7
commit be565bb945
12 changed files with 408 additions and 265 deletions
Generated
+20 -20
View File
@@ -1160,7 +1160,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_builtins" name = "oxide_builtins"
version = "0.15.6" version = "0.15.7"
dependencies = [ dependencies = [
"chrono", "chrono",
"fast-float", "fast-float",
@@ -1185,7 +1185,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_bytecode" name = "oxide_bytecode"
version = "0.15.6" version = "0.15.7"
dependencies = [ dependencies = [
"num-bigint", "num-bigint",
"oxide_log", "oxide_log",
@@ -1193,7 +1193,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_cfg" name = "oxide_cfg"
version = "0.15.6" version = "0.15.7"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_emit", "oxide_emit",
@@ -1204,7 +1204,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_cli" name = "oxide_cli"
version = "0.15.6" version = "0.15.7"
dependencies = [ dependencies = [
"ansi_term", "ansi_term",
"bumpalo", "bumpalo",
@@ -1228,7 +1228,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_code_cache" name = "oxide_code_cache"
version = "0.15.6" version = "0.15.7"
dependencies = [ dependencies = [
"lru", "lru",
"oxide_bytecode", "oxide_bytecode",
@@ -1237,7 +1237,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_compiler" name = "oxide_compiler"
version = "0.15.6" version = "0.15.7"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_cfg", "oxide_cfg",
@@ -1257,7 +1257,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_dce" name = "oxide_dce"
version = "0.15.6" version = "0.15.7"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_cfg", "oxide_cfg",
@@ -1271,7 +1271,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_emit" name = "oxide_emit"
version = "0.15.6" version = "0.15.7"
dependencies = [ dependencies = [
"num-bigint", "num-bigint",
"oxc_allocator", "oxc_allocator",
@@ -1286,7 +1286,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_engine" name = "oxide_engine"
version = "0.15.6" version = "0.15.7"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_compiler", "oxide_compiler",
@@ -1298,7 +1298,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_ir" name = "oxide_ir"
version = "0.15.6" version = "0.15.7"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_log", "oxide_log",
@@ -1307,7 +1307,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_kernel" name = "oxide_kernel"
version = "0.15.6" version = "0.15.7"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"dashmap", "dashmap",
@@ -1321,7 +1321,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_liveness" name = "oxide_liveness"
version = "0.15.6" version = "0.15.7"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_cfg", "oxide_cfg",
@@ -1333,7 +1333,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_log" name = "oxide_log"
version = "0.15.6" version = "0.15.7"
dependencies = [ dependencies = [
"tracing", "tracing",
"tracing-appender", "tracing-appender",
@@ -1342,11 +1342,11 @@ dependencies = [
[[package]] [[package]]
name = "oxide_net" name = "oxide_net"
version = "0.15.6" version = "0.15.7"
[[package]] [[package]]
name = "oxide_parser" name = "oxide_parser"
version = "0.15.6" version = "0.15.7"
dependencies = [ dependencies = [
"oxc_allocator", "oxc_allocator",
"oxc_ast", "oxc_ast",
@@ -1361,7 +1361,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_regalloc" name = "oxide_regalloc"
version = "0.15.6" version = "0.15.7"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_cfg", "oxide_cfg",
@@ -1377,7 +1377,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_runtime_api" name = "oxide_runtime_api"
version = "0.15.6" version = "0.15.7"
dependencies = [ dependencies = [
"num-bigint", "num-bigint",
"num-traits", "num-traits",
@@ -1390,7 +1390,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_test262" name = "oxide_test262"
version = "0.15.6" version = "0.15.7"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_compiler", "oxide_compiler",
@@ -1408,7 +1408,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_types" name = "oxide_types"
version = "0.15.6" version = "0.15.7"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"dashmap", "dashmap",
@@ -1421,7 +1421,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_vm" name = "oxide_vm"
version = "0.15.6" version = "0.15.7"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"chrono", "chrono",
+1 -1
View File
@@ -24,7 +24,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "0.15.6" version = "0.15.7"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
rust-version = "1.80" rust-version = "1.80"
+17 -7
View File
@@ -2,12 +2,14 @@
#![allow(dead_code)] #![allow(dead_code)]
use std::fs; use std::fs;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::process::ExitCode; use std::process::ExitCode;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use ansi_term::Colour::Red; use ansi_term::Colour::Red;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use oxide_cli::format_js_value; use oxide_cli::format_js_value;
use oxide_net::Endpoint;
use oxide_cli::server::cleanup::{well_known_cleanup, CleanupOutcome}; use oxide_cli::server::cleanup::{well_known_cleanup, CleanupOutcome};
use oxide_cli::server::client::send_control_request; use oxide_cli::server::client::send_control_request;
use oxide_cli::server::log as server_log; use oxide_cli::server::log as server_log;
@@ -123,6 +125,9 @@ enum ServerCommands {
/// 常驻 worker 线程数;缺省取宿主核数。 /// 常驻 worker 线程数;缺省取宿主核数。
#[arg(long)] #[arg(long)]
workers: Option<u32>, workers: Option<u32>,
/// TCP 监听端口;缺省取 well-known 端口。
#[arg(long, default_value_t = sidecar::WELL_KNOWN_PORT, conflicts_with = "rm")]
port: u16,
/// 前台形态:当前进程直接运行持久 server(守护形态 spawn 的子进程用, /// 前台形态:当前进程直接运行持久 server(守护形态 spawn 的子进程用,
/// 不向用户暴露)。 /// 不向用户暴露)。
#[arg(long, hide = true)] #[arg(long, hide = true)]
@@ -324,6 +329,7 @@ fn server_command(command: ServerCommands) -> ExitCode {
rm, rm,
idle_timeout, idle_timeout,
workers, workers,
port,
foreground, foreground,
} => { } => {
if rm { if rm {
@@ -336,13 +342,16 @@ fn server_command(command: ServerCommands) -> ExitCode {
} }
start_result(run_server_rm(&config)) start_result(run_server_rm(&config))
} else if foreground { } else if foreground {
let mut config = ServerConfig::well_known(); let mut config = ServerConfig {
endpoint: Endpoint::Tcp(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port)),
..ServerConfig::well_known()
};
if let Some(n) = workers { if let Some(n) = workers {
config.worker_count = n as usize; config.worker_count = n as usize;
} }
start_result(run_server(&config)) start_result(run_server(&config))
} else { } else {
start_daemon(workers) start_daemon(workers, port)
} }
} }
ServerCommands::Cleanup => match well_known_cleanup() { ServerCommands::Cleanup => match well_known_cleanup() {
@@ -382,7 +391,7 @@ fn server_command(command: ServerCommands) -> ExitCode {
}), }),
ServerCommands::Info => control_arm(&ServerRequest::Info, |response| match response { ServerCommands::Info => control_arm(&ServerRequest::Info, |response| match response {
ServerResponse::Info { version, socket_path, pid } => { ServerResponse::Info { version, socket_path, pid } => {
Some(format!("版本:{version}\nsocket:{socket_path}\n进程号:{pid}")) Some(format!("版本:{version}\nendpoint:{socket_path}\n进程号:{pid}"))
} }
_ => None, _ => None,
}), }),
@@ -491,9 +500,9 @@ fn control_arm(request: &ServerRequest, render: fn(&ServerResponse) -> Option<St
/// ///
/// # 副作用 /// # 副作用
/// - server 为脱离进程,CLI 退出后由 init 收领继续运行。 /// - server 为脱离进程,CLI 退出后由 init 收领继续运行。
fn start_daemon(workers: Option<u32>) -> ExitCode { fn start_daemon(workers: Option<u32>, port: u16) -> ExitCode {
// spawn:脱离子进程,标准流全空,不等待。 // spawn:脱离子进程,标准流全空,不等待。
if let Err(err) = spawn::spawn_detached_server(workers) { if let Err(err) = spawn::spawn_detached_server(workers, port) {
eprintln!("{}", Red.paint(format!("spawn server 进程失败:{err}"))); eprintln!("{}", Red.paint(format!("spawn server 进程失败:{err}")));
return ExitCode::FAILURE; return ExitCode::FAILURE;
} }
@@ -545,8 +554,9 @@ fn restart_command() -> ExitCode {
std::thread::sleep(Duration::from_millis(50)); std::thread::sleep(Duration::from_millis(50));
} }
// 拉起新 server:脱离进程,标准流全空,不等待。 // 拉起新 server:脱离进程,标准流全空,不等待。新 server 按缺省配置拉起
if let Err(err) = spawn::spawn_detached_server(None) { // (well-known 端口,不继承旧旗标)。
if let Err(err) = spawn::spawn_detached_server(None, sidecar::WELL_KNOWN_PORT) {
eprintln!("{}", Red.paint(format!("拉起新 server 失败:{err}"))); eprintln!("{}", Red.paint(format!("拉起新 server 失败:{err}")));
return ExitCode::FAILURE; return ExitCode::FAILURE;
} }
+54 -36
View File
@@ -113,7 +113,7 @@ pub fn decide(
/// 清理入口:扫 sidecar、探活、杀 PID、删孤儿文件。 /// 清理入口:扫 sidecar、探活、杀 PID、删孤儿文件。
/// ///
/// # 步骤 /// # 步骤
/// 1. 采集五个探活信号(sidecar 可解析性、socket 存活、socket 文件存在、 /// 1. 采集五个探活信号(sidecar 可解析性、endpoint 存活、sidecar 文件存在、
/// PID 存活、刻度匹配)。 /// PID 存活、刻度匹配)。
/// 2. 调 `decide` 得决策。 /// 2. 调 `decide` 得决策。
/// 3. 按决策落地(无操作 / 只删文件 / 先杀后删 / 拒绝)。 /// 3. 按决策落地(无操作 / 只删文件 / 先杀后删 / 拒绝)。
@@ -125,22 +125,24 @@ pub fn decide(
/// - 可能向 sidecar 记录的进程发 SIGTERM;可能删除 socket 与 sidecar 文件。 /// - 可能向 sidecar 记录的进程发 SIGTERM;可能删除 socket 与 sidecar 文件。
/// ///
/// # 注意事项 /// # 注意事项
/// - 两条保守拒绝底线:socket 存活但 PID 无法可靠确定、杀进程超时,均不 /// - 两条保守拒绝底线:endpoint 存活但 PID 无法可靠确定、杀进程超时,均不
/// 触碰文件。 /// 触碰文件。
pub fn cleanup(sidecar_path: &Path, socket_path: &Path) -> CleanupOutcome { pub fn cleanup(sidecar_path: &Path, endpoint: &Endpoint) -> CleanupOutcome {
cleanup_with_wait(sidecar_path, socket_path, DEFAULT_KILL_WAIT) cleanup_with_wait(sidecar_path, endpoint, DEFAULT_KILL_WAIT)
} }
/// 清理入口(可注入杀进程等待,测试注入短超时)。 /// 清理入口(可注入杀进程等待,测试注入短超时)。
/// ///
/// # 步骤 /// # 步骤
/// 1. 采集信号:`read_identity`(缺失与损坏同归 `None`)、socket 文件存在、 /// 1. 采集信号:`read_identity`(缺失与损坏同归 `None`)、sidecar 文件存在、
/// `is_server_alive`(仅文件存在时探活)、`pid_alive`、`process_starttime` /// `is_server_alive`(探 sidecar 记录的 endpoint,不可读时探调用方
/// 与 sidecar 记录值比对(PID 死时匹配位恒假)。 /// endpoint 兜底)、`pid_alive`、`process_starttime` 与 sidecar 记录值比对
/// (PID 死时匹配位恒假)。
/// 2. 调 `decide` 得决策。 /// 2. 调 `decide` 得决策。
/// 3. 按决策落地:无操作返回 `Clean`;只删文件返回 `Removed`;先杀后删在 /// 3. 按决策落地:无操作返回 `Clean`(Unix 臂 sidecar 缺失而 socket 文件存在时
/// 信号发送失败或等待超时时返回 `KillFailed`(文件不碰)、成功返回 /// 按孤儿删除返回 `Removed`);只删文件返回 `Removed`;先杀后删在信号发送
/// `Removed`;拒绝返回 `RefusedLiveServer`(文件不碰)。 /// 失败或等待超时时返回 `KillFailed`(文件不碰)、成功返回 `Removed`;拒绝
/// 返回 `RefusedLiveServer`(文件不碰)。
/// ///
/// # 边界与前提 /// # 边界与前提
/// - `kill_wait` 是 SIGTERM 送达后等进程退出的窗口;server 走信号处理的 /// - `kill_wait` 是 SIGTERM 送达后等进程退出的窗口;server 走信号处理的
@@ -148,12 +150,17 @@ pub fn cleanup(sidecar_path: &Path, socket_path: &Path) -> CleanupOutcome {
/// ///
/// # 副作用 /// # 副作用
/// - 可能向 sidecar 记录的进程发 SIGTERM;可能删除 socket 与 sidecar 文件。 /// - 可能向 sidecar 记录的进程发 SIGTERM;可能删除 socket 与 sidecar 文件。
pub fn cleanup_with_wait(sidecar_path: &Path, socket_path: &Path, kill_wait: Duration) -> CleanupOutcome { pub fn cleanup_with_wait(sidecar_path: &Path, endpoint: &Endpoint, kill_wait: Duration) -> CleanupOutcome {
// 采集信号:sidecar 可解析性(缺失与损坏同归 None)、socket 存活、 // 采集信号:sidecar 可解析性(缺失与损坏同归 None)、endpoint 存活、
// PID 存活与刻度匹配(PID 死时匹配位恒假)。 // PID 存活与刻度匹配(PID 死时匹配位恒假)。endpoint 探活目标取 sidecar
// 记录的 endpoint(正确性红线),不可读时取调用方 endpoint(防御性兜底)。
let identity = sidecar::read_identity(sidecar_path); let identity = sidecar::read_identity(sidecar_path);
let socket_exists = socket_path.exists(); let registry_exists = sidecar_path.exists();
let socket_alive = socket_exists && sidecar::is_server_alive(&Endpoint::Unix(socket_path.to_path_buf())); let probe = match &identity {
Some(id) => sidecar::parse_endpoint(&id.socket_path),
None => endpoint.clone(),
};
let endpoint_alive = sidecar::is_server_alive(&probe);
let (pid_alive, starttime_match) = match &identity { let (pid_alive, starttime_match) = match &identity {
Some(id) => { Some(id) => {
let alive = sidecar::pid_alive(id.pid); let alive = sidecar::pid_alive(id.pid);
@@ -163,13 +170,22 @@ pub fn cleanup_with_wait(sidecar_path: &Path, socket_path: &Path, kill_wait: Dur
}; };
// 裁决:五个信号映射到四态决策。 // 裁决:五个信号映射到四态决策。
let decision = decide(identity.as_ref(), socket_exists, socket_alive, pid_alive, starttime_match); let decision = decide(identity.as_ref(), registry_exists, endpoint_alive, pid_alive, starttime_match);
// 按决策落地。 // 按决策落地。
match decision { match decision {
Decision::None => CleanupOutcome::Clean, Decision::None => {
// 遗留信号:Unix 臂 sidecar 缺失而 socket 文件存在,按孤儿删除。
if let Some(path) = endpoint.socket_path() {
if !registry_exists && path.exists() {
let _ = fs::remove_file(path);
return CleanupOutcome::Removed;
}
}
CleanupOutcome::Clean
}
Decision::DeleteOnly => { Decision::DeleteOnly => {
remove_files(sidecar_path, socket_path); remove_files(sidecar_path, endpoint);
CleanupOutcome::Removed CleanupOutcome::Removed
} }
Decision::Kill => { Decision::Kill => {
@@ -183,20 +199,19 @@ pub fn cleanup_with_wait(sidecar_path: &Path, socket_path: &Path, kill_wait: Dur
return CleanupOutcome::KillFailed; return CleanupOutcome::KillFailed;
} }
// server 自身退出序列已删文件时删除报错被忽略(尽力而为的残留兜底)。 // server 自身退出序列已删文件时删除报错被忽略(尽力而为的残留兜底)。
remove_files(sidecar_path, socket_path); remove_files(sidecar_path, endpoint);
CleanupOutcome::Removed CleanupOutcome::Removed
} }
Decision::Refuse => CleanupOutcome::RefusedLiveServer, Decision::Refuse => CleanupOutcome::RefusedLiveServer,
} }
} }
/// well-known 路径清理入口:解析 well-known sidecar 与 socket 路径后调 /// well-known 路径清理入口:解析 well-known sidecar 与 endpoint 后调 `cleanup`。
/// `cleanup`。
/// ///
/// # 注意事项 /// # 注意事项
/// - CLI 分派臂与启动 liveness 扫描的稳定入口。 /// - CLI 分派臂与启动 liveness 扫描的稳定入口。
pub fn well_known_cleanup() -> CleanupOutcome { pub fn well_known_cleanup() -> CleanupOutcome {
cleanup(&sidecar::well_known_sidecar_path(), &sidecar::well_known_socket_path()) cleanup(&sidecar::well_known_sidecar_path(), &sidecar::well_known_endpoint())
} }
/// 发送 SIGTERM:`kill -TERM`,与 `pid_alive` 的 `kill -0` 同一 `Command` 路径。 /// 发送 SIGTERM:`kill -TERM`,与 `pid_alive` 的 `kill -0` 同一 `Command` 路径。
@@ -227,13 +242,16 @@ pub fn wait_pid_exit(pid: u32, timeout: Duration) -> bool {
} }
/// 删除残留文件:socket 先、sidecar 后(与交接退出序列同一定序:新 server /// 删除残留文件:socket 先、sidecar 后(与交接退出序列同一定序:新 server
/// 以 sidecar 消失为接管信号,届时 socket 文件必已删除)。 /// 以 sidecar 消失为接管信号,届时 socket 文件必已删除)。TCP 臂无文件可删,
/// 只删 sidecar。
/// ///
/// # 副作用 /// # 副作用
/// - 可能删除 socket 文件与 sidecar 文件;文件已不存在不视为错误(server /// - 可能删除 socket 文件(Unix 臂)与 sidecar 文件;文件已不存在不视为
/// 自身退出序列可能已删过)。 /// 错误(server 自身退出序列可能已删过)。
pub fn remove_files(sidecar_path: &Path, socket_path: &Path) { pub fn remove_files(sidecar_path: &Path, endpoint: &Endpoint) {
let _ = fs::remove_file(socket_path); if let Some(path) = endpoint.socket_path() {
let _ = fs::remove_file(path);
}
let _ = fs::remove_file(sidecar_path); let _ = fs::remove_file(sidecar_path);
} }
@@ -338,7 +356,7 @@ mod tests {
#[test] #[test]
fn cleanup_no_files_clean() { fn cleanup_no_files_clean() {
let dir = TestDir::new(); let dir = TestDir::new();
let outcome = cleanup(&dir.path("sidecar.json"), &dir.path("server.sock")); let outcome = cleanup(&dir.path("sidecar.json"), &Endpoint::Unix(dir.path("server.sock")));
assert_eq!(outcome, CleanupOutcome::Clean, "无文件应判无残留"); assert_eq!(outcome, CleanupOutcome::Clean, "无文件应判无残留");
} }
@@ -349,7 +367,7 @@ mod tests {
let sidecar = dir.path("sidecar.json"); let sidecar = dir.path("sidecar.json");
let socket = dir.path("server.sock"); let socket = dir.path("server.sock");
fs::write(&socket, b"stale").expect("写陈旧 socket 文件应成功"); fs::write(&socket, b"stale").expect("写陈旧 socket 文件应成功");
let outcome = cleanup(&sidecar, &socket); let outcome = cleanup(&sidecar, &Endpoint::Unix(socket.clone()));
assert_eq!(outcome, CleanupOutcome::Removed, "孤儿 socket 文件应判已清理"); assert_eq!(outcome, CleanupOutcome::Removed, "孤儿 socket 文件应判已清理");
assert!(!socket.exists(), "socket 文件应被删除"); assert!(!socket.exists(), "socket 文件应被删除");
assert!(!sidecar.exists(), "sidecar 不应被创建"); assert!(!sidecar.exists(), "sidecar 不应被创建");
@@ -362,7 +380,7 @@ mod tests {
let sidecar = dir.path("sidecar.json"); let sidecar = dir.path("sidecar.json");
let socket = dir.path("server.sock"); let socket = dir.path("server.sock");
sidecar::write_exclusive(&test_identity(dead_pid(), 0, &socket), &sidecar).expect("写 sidecar 应成功"); sidecar::write_exclusive(&test_identity(dead_pid(), 0, &socket), &sidecar).expect("写 sidecar 应成功");
let outcome = cleanup(&sidecar, &socket); let outcome = cleanup(&sidecar, &Endpoint::Unix(socket.clone()));
assert_eq!(outcome, CleanupOutcome::Removed, "死 PID 残留应判已清理"); assert_eq!(outcome, CleanupOutcome::Removed, "死 PID 残留应判已清理");
assert!(!sidecar.exists(), "sidecar 应被删除"); assert!(!sidecar.exists(), "sidecar 应被删除");
} }
@@ -375,7 +393,7 @@ mod tests {
let socket = dir.path("server.sock"); let socket = dir.path("server.sock");
fs::write(&sidecar, "not json at all").expect("写损坏文件应成功"); fs::write(&sidecar, "not json at all").expect("写损坏文件应成功");
fs::write(&socket, b"stale").expect("写陈旧 socket 文件应成功"); fs::write(&socket, b"stale").expect("写陈旧 socket 文件应成功");
let outcome = cleanup(&sidecar, &socket); let outcome = cleanup(&sidecar, &Endpoint::Unix(socket.clone()));
assert_eq!(outcome, CleanupOutcome::Removed, "损坏 sidecar 加陈旧 socket 应判已清理"); assert_eq!(outcome, CleanupOutcome::Removed, "损坏 sidecar 加陈旧 socket 应判已清理");
assert!(!sidecar.exists(), "sidecar 应被删除"); assert!(!sidecar.exists(), "sidecar 应被删除");
assert!(!socket.exists(), "socket 文件应被删除"); assert!(!socket.exists(), "socket 文件应被删除");
@@ -389,7 +407,7 @@ mod tests {
let socket = dir.path("server.sock"); let socket = dir.path("server.sock");
let listener = UnixListener::bind(&socket).expect("绑定监听者应成功"); let listener = UnixListener::bind(&socket).expect("绑定监听者应成功");
fs::write(&sidecar, "not json at all").expect("写损坏文件应成功"); fs::write(&sidecar, "not json at all").expect("写损坏文件应成功");
let outcome = cleanup(&sidecar, &socket); let outcome = cleanup(&sidecar, &Endpoint::Unix(socket.clone()));
assert_eq!(outcome, CleanupOutcome::RefusedLiveServer, "损坏 sidecar 加存活 socket 应判拒绝"); assert_eq!(outcome, CleanupOutcome::RefusedLiveServer, "损坏 sidecar 加存活 socket 应判拒绝");
assert!(sidecar.exists(), "sidecar 应原样"); assert!(sidecar.exists(), "sidecar 应原样");
assert!(socket.exists(), "socket 文件应原样"); assert!(socket.exists(), "socket 文件应原样");
@@ -404,7 +422,7 @@ mod tests {
let sidecar = dir.path("sidecar.json"); let sidecar = dir.path("sidecar.json");
let socket = dir.path("server.sock"); let socket = dir.path("server.sock");
sidecar::write_exclusive(&test_identity(std::process::id(), 0, &socket), &sidecar).expect("写 sidecar 应成功"); sidecar::write_exclusive(&test_identity(std::process::id(), 0, &socket), &sidecar).expect("写 sidecar 应成功");
let outcome = cleanup(&sidecar, &socket); let outcome = cleanup(&sidecar, &Endpoint::Unix(socket.clone()));
assert_eq!(outcome, CleanupOutcome::Removed, "刻度失配残留应判已清理"); assert_eq!(outcome, CleanupOutcome::Removed, "刻度失配残留应判已清理");
assert!(!sidecar.exists(), "sidecar 应被删除"); assert!(!sidecar.exists(), "sidecar 应被删除");
assert!(sidecar::pid_alive(std::process::id()), "本进程应不受影响"); assert!(sidecar::pid_alive(std::process::id()), "本进程应不受影响");
@@ -419,7 +437,7 @@ mod tests {
let socket = dir.path("server.sock"); let socket = dir.path("server.sock");
let listener = UnixListener::bind(&socket).expect("绑定监听者应成功"); let listener = UnixListener::bind(&socket).expect("绑定监听者应成功");
sidecar::write_exclusive(&test_identity(std::process::id(), 0, &socket), &sidecar).expect("写 sidecar 应成功"); sidecar::write_exclusive(&test_identity(std::process::id(), 0, &socket), &sidecar).expect("写 sidecar 应成功");
let outcome = cleanup(&sidecar, &socket); let outcome = cleanup(&sidecar, &Endpoint::Unix(socket.clone()));
assert_eq!(outcome, CleanupOutcome::RefusedLiveServer, "存活 socket 加刻度失配应判拒绝"); assert_eq!(outcome, CleanupOutcome::RefusedLiveServer, "存活 socket 加刻度失配应判拒绝");
assert!(sidecar.exists(), "sidecar 应原样"); assert!(sidecar.exists(), "sidecar 应原样");
assert!(socket.exists(), "socket 文件应原样"); assert!(socket.exists(), "socket 文件应原样");
@@ -442,7 +460,7 @@ mod tests {
}); });
let id = test_identity(pid, sidecar::process_starttime(pid).expect("子进程启动刻度应可读"), &socket); let id = test_identity(pid, sidecar::process_starttime(pid).expect("子进程启动刻度应可读"), &socket);
sidecar::write_exclusive(&id, &sidecar).expect("写 sidecar 应成功"); sidecar::write_exclusive(&id, &sidecar).expect("写 sidecar 应成功");
let outcome = cleanup_with_wait(&sidecar, &socket, Duration::from_secs(3)); let outcome = cleanup_with_wait(&sidecar, &Endpoint::Unix(socket.clone()), Duration::from_secs(3));
assert_eq!(outcome, CleanupOutcome::Removed, "刻度匹配的存活进程应被杀且文件被删"); assert_eq!(outcome, CleanupOutcome::Removed, "刻度匹配的存活进程应被杀且文件被删");
// 有界等待回收线程:子进程被杀后收尸即退出,不无限阻塞。 // 有界等待回收线程:子进程被杀后收尸即退出,不无限阻塞。
let deadline = Instant::now() + Duration::from_secs(5); let deadline = Instant::now() + Duration::from_secs(5);
@@ -469,7 +487,7 @@ mod tests {
let pid = child.id(); let pid = child.id();
let id = test_identity(pid, sidecar::process_starttime(pid).expect("子进程启动刻度应可读"), &socket); let id = test_identity(pid, sidecar::process_starttime(pid).expect("子进程启动刻度应可读"), &socket);
sidecar::write_exclusive(&id, &sidecar).expect("写 sidecar 应成功"); sidecar::write_exclusive(&id, &sidecar).expect("写 sidecar 应成功");
let outcome = cleanup_with_wait(&sidecar, &socket, Duration::from_millis(300)); let outcome = cleanup_with_wait(&sidecar, &Endpoint::Unix(socket.clone()), Duration::from_millis(300));
assert_eq!(outcome, CleanupOutcome::KillFailed, "忽略 SIGTERM 应判杀进程失败"); assert_eq!(outcome, CleanupOutcome::KillFailed, "忽略 SIGTERM 应判杀进程失败");
assert!(sidecar.exists(), "sidecar 应原样"); assert!(sidecar.exists(), "sidecar 应原样");
assert!(!socket.exists(), "socket 文件应保持不存在"); assert!(!socket.exists(), "socket 文件应保持不存在");
+5 -4
View File
@@ -11,7 +11,7 @@
use std::io::BufReader; use std::io::BufReader;
use std::net::TcpStream; use std::net::TcpStream;
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf}; use std::path::Path;
use std::sync::mpsc::RecvTimeoutError; use std::sync::mpsc::RecvTimeoutError;
use std::time::Duration; use std::time::Duration;
@@ -78,8 +78,9 @@ pub fn send_control_request_from(sidecar_path: &Path, request: &ServerRequest) -
)) ))
})?; })?;
// 连接:5 秒超时是安全网,本地 socket 连接立即完成或立即失败。 // 连接:5 秒超时是安全网,本地连接立即完成或立即失败。endpoint 取 sidecar
let mut stream = connect_with_timeout(&Endpoint::Unix(PathBuf::from(identity.socket_path)), CONNECT_TIMEOUT)?; // 记录的文本形态(host:port 或路径),撤掉 Unix 过渡包装。
let mut stream = connect_with_timeout(&sidecar::parse_endpoint(&identity.socket_path), CONNECT_TIMEOUT)?;
// 读写超时各 5 秒:server 挂起(不回应)时不无限阻塞。 // 读写超时各 5 秒:server 挂起(不回应)时不无限阻塞。
let _ = stream.set_read_timeout(Some(CONNECT_TIMEOUT)); let _ = stream.set_read_timeout(Some(CONNECT_TIMEOUT));
@@ -181,7 +182,7 @@ mod tests {
/// 写一个指向给定 socket 路径的合法 sidecar。 /// 写一个指向给定 socket 路径的合法 sidecar。
fn write_sidecar(sidecar: &Path, socket: &Path) { fn write_sidecar(sidecar: &Path, socket: &Path) {
let identity = sidecar::ServerIdentity::new(socket.to_str().unwrap(), "0.0.0"); let identity = sidecar::ServerIdentity::new(&Endpoint::Unix(socket.to_path_buf()), "0.0.0");
sidecar::write_exclusive(&identity, sidecar).expect("写 sidecar 应成功"); sidecar::write_exclusive(&identity, sidecar).expect("写 sidecar 应成功");
} }
+51 -36
View File
@@ -44,16 +44,16 @@ pub enum LivenessOutcome {
KillFailed, KillFailed,
} }
/// 启动时 liveness 扫描入口:扫 sidecar 与 socket,清理陈旧残留,僵尸态自动恢复。 /// 启动时 liveness 扫描入口:扫 sidecar 与 endpoint,清理陈旧残留,僵尸态自动恢复。
/// ///
/// # 步骤 /// # 步骤
/// 1. 采集五个探活信号(sidecar 可解析性、socket 存活、socket 文件存在、 /// 1. 采集五个探活信号(sidecar 可解析性、endpoint 存活、sidecar 文件存在、
/// PID 存活、刻度匹配)。 /// PID 存活、刻度匹配)。
/// 2. socket 存活守卫:存活即无操作(自动扫描绝不杀存活 server)。 /// 2. endpoint 存活守卫:存活即无操作(自动扫描绝不杀存活 server)。
/// 3. 按 `decide` 裁决表分派:无操作 / 只删文件 / 僵尸分支 / 拒绝。 /// 3. 按 `decide` 裁决表分派:无操作 / 只删文件 / 僵尸分支 / 拒绝。
/// ///
/// # 边界与前提 /// # 边界与前提
/// - 只删 `config` 注入的 sidecar 与 socket 两条路径;日志文件不碰。 /// - 只删 `config` 注入的 sidecar 与 socket 两条路径(Unix 臂);日志文件不碰。
/// - 杀进程等待取缺省 10 秒(长于 server 的在途排水安全超时 5 秒)。 /// - 杀进程等待取缺省 10 秒(长于 server 的在途排水安全超时 5 秒)。
/// ///
/// # 副作用 /// # 副作用
@@ -61,20 +61,21 @@ pub enum LivenessOutcome {
/// ///
/// # 注意事项 /// # 注意事项
/// - 须在 `claim_sidecar` 之前调用(认领裁决前清掉陈旧状态)。 /// - 须在 `claim_sidecar` 之前调用(认领裁决前清掉陈旧状态)。
pub fn liveness_scan(sidecar_path: &Path, socket_path: &Path) -> LivenessOutcome { pub fn liveness_scan(sidecar_path: &Path, endpoint: &Endpoint) -> LivenessOutcome {
liveness_scan_with_waits(sidecar_path, socket_path, ZOMBIE_EXIT_WINDOW, cleanup::DEFAULT_KILL_WAIT) liveness_scan_with_waits(sidecar_path, endpoint, ZOMBIE_EXIT_WINDOW, cleanup::DEFAULT_KILL_WAIT)
} }
/// 启动时 liveness 扫描(可注入两个超时,测试注入短窗口)。 /// 启动时 liveness 扫描(可注入两个超时,测试注入短窗口)。
/// ///
/// # 步骤 /// # 步骤
/// 1. 采集信号:`read_identity`(缺失与损坏同归 `None`)、socket 文件存在、 /// 1. 采集信号:`read_identity`(缺失与损坏同归 `None`)、sidecar 文件存在、
/// `is_server_alive`(仅文件存在时探活)、`pid_alive`、`process_starttime` /// `is_server_alive`(探 sidecar 记录的 endpoint,不可读时探调用方
/// 与 sidecar 记录值比对(PID 死时匹配位恒假)。 /// endpoint 兜底)、`pid_alive`、`process_starttime` 与 sidecar 记录值比对
/// 2. socket 存活守卫:存活即无操作。 /// (PID 死时匹配位恒假)。
/// 2. endpoint 存活守卫:存活即无操作。
/// 3. 按 `decide` 裁决表分派: /// 3. 按 `decide` 裁决表分派:
/// - `None` 无操作。 /// - `None` 无操作(Unix 臂 sidecar 缺失而 socket 文件存在时按孤儿删除)。
/// - `DeleteOnly` 删文件(socket 先、sidecar 后)。 /// - `DeleteOnly` 删文件(socket 先、sidecar 后,Unix 臂)。
/// - `Kill` 僵尸分支:有界等待 sidecar 消失,仍在则杀进程后删文件。 /// - `Kill` 僵尸分支:有界等待 sidecar 消失,仍在则杀进程后删文件。
/// - `Refuse` 无操作(文件不碰,归 `claim_sidecar` 裁决)。 /// - `Refuse` 无操作(文件不碰,归 `claim_sidecar` 裁决)。
/// ///
@@ -88,13 +89,18 @@ pub fn liveness_scan(sidecar_path: &Path, socket_path: &Path) -> LivenessOutcome
/// - 可能向 sidecar 记录的进程发 SIGTERM;可能删除 socket 与 sidecar 文件。 /// - 可能向 sidecar 记录的进程发 SIGTERM;可能删除 socket 与 sidecar 文件。
/// - 杀进程决策与失败记入跨重启保留的日志文件(守护形态 stderr 是 null)。 /// - 杀进程决策与失败记入跨重启保留的日志文件(守护形态 stderr 是 null)。
pub fn liveness_scan_with_waits( pub fn liveness_scan_with_waits(
sidecar_path: &Path, socket_path: &Path, zombie_exit_window: Duration, kill_wait: Duration, sidecar_path: &Path, endpoint: &Endpoint, zombie_exit_window: Duration, kill_wait: Duration,
) -> LivenessOutcome { ) -> LivenessOutcome {
// 采集信号:sidecar 可解析性(缺失与损坏同归 None)、socket 存活、 // 采集信号:sidecar 可解析性(缺失与损坏同归 None)、endpoint 存活、
// PID 存活与刻度匹配(PID 死时匹配位恒假)。 // PID 存活与刻度匹配(PID 死时匹配位恒假)。endpoint 探活目标取 sidecar
// 记录的 endpoint(正确性红线),不可读时取调用方 endpoint(防御性兜底)。
let identity = sidecar::read_identity(sidecar_path); let identity = sidecar::read_identity(sidecar_path);
let socket_exists = socket_path.exists(); let registry_exists = sidecar_path.exists();
let socket_alive = socket_exists && sidecar::is_server_alive(&Endpoint::Unix(socket_path.to_path_buf())); let probe = match &identity {
Some(id) => sidecar::parse_endpoint(&id.socket_path),
None => endpoint.clone(),
};
let endpoint_alive = sidecar::is_server_alive(&probe);
let (pid_alive, starttime_match) = match &identity { let (pid_alive, starttime_match) = match &identity {
Some(id) => { Some(id) => {
let alive = sidecar::pid_alive(id.pid); let alive = sidecar::pid_alive(id.pid);
@@ -103,19 +109,28 @@ pub fn liveness_scan_with_waits(
None => (false, false), None => (false, false),
}; };
// socket 存活守卫:存活 server 的处置归 `claim_sidecar` 裁决(同版本 // endpoint 存活守卫:存活 server 的处置归 `claim_sidecar` 裁决(同版本
// AlreadyRunning 拒绝、异版本 VersionMismatch 走交接)。自动扫描绝不杀 // AlreadyRunning 拒绝、异版本 VersionMismatch 走交接)。自动扫描绝不杀
// 存活 server:`decide` 表的 Kill 行人工清理语义,杀存活 server 等于抢占, // 存活 server:`decide` 表的 Kill 行人工清理语义,杀存活 server 等于抢占,
// 违反单例交接协议。 // 违反单例交接协议。
if socket_alive { if endpoint_alive {
return LivenessOutcome::Noop; return LivenessOutcome::Noop;
} }
// 按裁决表分派。 // 按裁决表分派。
match cleanup::decide(identity.as_ref(), socket_exists, socket_alive, pid_alive, starttime_match) { match cleanup::decide(identity.as_ref(), registry_exists, endpoint_alive, pid_alive, starttime_match) {
Decision::None => LivenessOutcome::Noop, Decision::None => {
// 遗留信号:Unix 臂 sidecar 缺失而 socket 文件存在,按孤儿删除。
if let Some(path) = endpoint.socket_path() {
if !registry_exists && path.exists() {
let _ = std::fs::remove_file(path);
return LivenessOutcome::Cleaned;
}
}
LivenessOutcome::Noop
}
Decision::DeleteOnly => { Decision::DeleteOnly => {
cleanup::remove_files(sidecar_path, socket_path); cleanup::remove_files(sidecar_path, endpoint);
LivenessOutcome::Cleaned LivenessOutcome::Cleaned
} }
Decision::Kill => { Decision::Kill => {
@@ -124,7 +139,7 @@ pub fn liveness_scan_with_waits(
let Some(id) = identity else { let Some(id) = identity else {
return LivenessOutcome::Noop; return LivenessOutcome::Noop;
}; };
kill_zombie(&id, sidecar_path, socket_path, zombie_exit_window, kill_wait) kill_zombie(&id, sidecar_path, endpoint, zombie_exit_window, kill_wait)
} }
Decision::Refuse => LivenessOutcome::Noop, Decision::Refuse => LivenessOutcome::Noop,
} }
@@ -148,7 +163,7 @@ pub fn liveness_scan_with_waits(
/// - 可能向 sidecar 记录的进程发 SIGTERM;可能删除 socket 与 sidecar 文件。 /// - 可能向 sidecar 记录的进程发 SIGTERM;可能删除 socket 与 sidecar 文件。
/// - 杀进程决策与失败记入跨重启保留的日志文件。 /// - 杀进程决策与失败记入跨重启保留的日志文件。
fn kill_zombie( fn kill_zombie(
id: &sidecar::ServerIdentity, sidecar_path: &Path, socket_path: &Path, zombie_exit_window: Duration, id: &sidecar::ServerIdentity, sidecar_path: &Path, endpoint: &Endpoint, zombie_exit_window: Duration,
kill_wait: Duration, kill_wait: Duration,
) -> LivenessOutcome { ) -> LivenessOutcome {
// 有界等待 sidecar 消失:覆盖旧 server 退出序列窗口(排水 5 秒,sidecar // 有界等待 sidecar 消失:覆盖旧 server 退出序列窗口(排水 5 秒,sidecar
@@ -184,7 +199,7 @@ fn kill_zombie(
} }
// 进程已退出:删文件(尽力而为;旧 server 的退出序列自己删文件,此处兜底)。 // 进程已退出:删文件(尽力而为;旧 server 的退出序列自己删文件,此处兜底)。
cleanup::remove_files(sidecar_path, socket_path); cleanup::remove_files(sidecar_path, endpoint);
oxide_log::__log_event!( oxide_log::__log_event!(
"oxide::kernel", "oxide::kernel",
oxide_log::tracing::Level::INFO, oxide_log::tracing::Level::INFO,
@@ -250,7 +265,7 @@ mod tests {
#[test] #[test]
fn no_residue_noop() { fn no_residue_noop() {
let dir = TestDir::new(); let dir = TestDir::new();
let outcome = liveness_scan(&dir.path("sidecar.json"), &dir.path("server.sock")); let outcome = liveness_scan(&dir.path("sidecar.json"), &Endpoint::Unix(dir.path("server.sock")));
assert_eq!(outcome, LivenessOutcome::Noop, "无文件应判无操作"); assert_eq!(outcome, LivenessOutcome::Noop, "无文件应判无操作");
} }
@@ -261,7 +276,7 @@ mod tests {
let sidecar = dir.path("sidecar.json"); let sidecar = dir.path("sidecar.json");
let socket = dir.path("server.sock"); let socket = dir.path("server.sock");
fs::write(&socket, b"stale").expect("写陈旧 socket 文件应成功"); fs::write(&socket, b"stale").expect("写陈旧 socket 文件应成功");
let outcome = liveness_scan(&sidecar, &socket); let outcome = liveness_scan(&sidecar, &Endpoint::Unix(socket.clone()));
assert_eq!(outcome, LivenessOutcome::Cleaned, "孤儿 socket 文件应判已清理"); assert_eq!(outcome, LivenessOutcome::Cleaned, "孤儿 socket 文件应判已清理");
assert!(!socket.exists(), "socket 文件应被删除"); assert!(!socket.exists(), "socket 文件应被删除");
assert!(!sidecar.exists(), "sidecar 不应被创建"); assert!(!sidecar.exists(), "sidecar 不应被创建");
@@ -274,7 +289,7 @@ mod tests {
let sidecar = dir.path("sidecar.json"); let sidecar = dir.path("sidecar.json");
let socket = dir.path("server.sock"); let socket = dir.path("server.sock");
sidecar::write_exclusive(&test_identity(dead_pid(), 0, &socket), &sidecar).expect("写 sidecar 应成功"); sidecar::write_exclusive(&test_identity(dead_pid(), 0, &socket), &sidecar).expect("写 sidecar 应成功");
let outcome = liveness_scan(&sidecar, &socket); let outcome = liveness_scan(&sidecar, &Endpoint::Unix(socket.clone()));
assert_eq!(outcome, LivenessOutcome::Cleaned, "死 PID 残留应判已清理"); assert_eq!(outcome, LivenessOutcome::Cleaned, "死 PID 残留应判已清理");
assert!(!sidecar.exists(), "sidecar 应被删除"); assert!(!sidecar.exists(), "sidecar 应被删除");
assert!(!socket.exists(), "socket 文件应保持不存在"); assert!(!socket.exists(), "socket 文件应保持不存在");
@@ -288,7 +303,7 @@ mod tests {
let sidecar = dir.path("sidecar.json"); let sidecar = dir.path("sidecar.json");
let socket = dir.path("server.sock"); let socket = dir.path("server.sock");
sidecar::write_exclusive(&test_identity(std::process::id(), 0, &socket), &sidecar).expect("写 sidecar 应成功"); sidecar::write_exclusive(&test_identity(std::process::id(), 0, &socket), &sidecar).expect("写 sidecar 应成功");
let outcome = liveness_scan(&sidecar, &socket); let outcome = liveness_scan(&sidecar, &Endpoint::Unix(socket.clone()));
assert_eq!(outcome, LivenessOutcome::Cleaned, "刻度失配残留应判已清理"); assert_eq!(outcome, LivenessOutcome::Cleaned, "刻度失配残留应判已清理");
assert!(!sidecar.exists(), "sidecar 应被删除"); assert!(!sidecar.exists(), "sidecar 应被删除");
assert!(sidecar::pid_alive(std::process::id()), "本进程应不受影响"); assert!(sidecar::pid_alive(std::process::id()), "本进程应不受影响");
@@ -302,7 +317,7 @@ mod tests {
let socket = dir.path("server.sock"); let socket = dir.path("server.sock");
fs::write(&sidecar, "not json at all").expect("写损坏文件应成功"); fs::write(&sidecar, "not json at all").expect("写损坏文件应成功");
fs::write(&socket, b"stale").expect("写陈旧 socket 文件应成功"); fs::write(&socket, b"stale").expect("写陈旧 socket 文件应成功");
let outcome = liveness_scan(&sidecar, &socket); let outcome = liveness_scan(&sidecar, &Endpoint::Unix(socket.clone()));
assert_eq!(outcome, LivenessOutcome::Cleaned, "损坏 sidecar 加陈旧 socket 应判已清理"); assert_eq!(outcome, LivenessOutcome::Cleaned, "损坏 sidecar 加陈旧 socket 应判已清理");
assert!(!sidecar.exists(), "sidecar 应被删除"); assert!(!sidecar.exists(), "sidecar 应被删除");
assert!(!socket.exists(), "socket 文件应被删除"); assert!(!socket.exists(), "socket 文件应被删除");
@@ -316,9 +331,9 @@ mod tests {
let sidecar = dir.path("sidecar.json"); let sidecar = dir.path("sidecar.json");
let socket = dir.path("server.sock"); let socket = dir.path("server.sock");
let listener = UnixListener::bind(&socket).expect("绑定监听者应成功"); let listener = UnixListener::bind(&socket).expect("绑定监听者应成功");
sidecar::write_exclusive(&sidecar::ServerIdentity::new(socket.to_str().unwrap(), "0.0.0"), &sidecar) sidecar::write_exclusive(&sidecar::ServerIdentity::new(&Endpoint::Unix(socket.clone()), "0.0.0"), &sidecar)
.expect("写 sidecar 应成功"); .expect("写 sidecar 应成功");
let outcome = liveness_scan(&sidecar, &socket); let outcome = liveness_scan(&sidecar, &Endpoint::Unix(socket.clone()));
assert_eq!(outcome, LivenessOutcome::Noop, "存活 socket 应判无操作"); assert_eq!(outcome, LivenessOutcome::Noop, "存活 socket 应判无操作");
assert!(sidecar.exists(), "sidecar 应原样"); assert!(sidecar.exists(), "sidecar 应原样");
assert!(socket.exists(), "socket 文件应原样"); assert!(socket.exists(), "socket 文件应原样");
@@ -333,13 +348,13 @@ mod tests {
let sidecar = dir.path("sidecar.json"); let sidecar = dir.path("sidecar.json");
let socket = dir.path("server.sock"); let socket = dir.path("server.sock");
// sidecar 记本进程 PID 加真实刻度(进程活且刻度匹配)、无 socket。 // sidecar 记本进程 PID 加真实刻度(进程活且刻度匹配)、无 socket。
let id = sidecar::ServerIdentity::new(socket.to_str().unwrap(), "0.0.0"); let id = sidecar::ServerIdentity::new(&Endpoint::Unix(socket.clone()), "0.0.0");
sidecar::write_exclusive(&id, &sidecar).expect("写 sidecar 应成功"); sidecar::write_exclusive(&id, &sidecar).expect("写 sidecar 应成功");
// 扫描线程:sidecar 存在时进入僵尸分支,有界等待 sidecar 消失。 // 扫描线程:sidecar 存在时进入僵尸分支,有界等待 sidecar 消失。
let sidecar_scan = sidecar.clone(); let sidecar_scan = sidecar.clone();
let socket_scan = socket.clone(); let socket_scan = socket.clone();
let scan_handle = std::thread::spawn(move || { let scan_handle = std::thread::spawn(move || {
liveness_scan_with_waits(&sidecar_scan, &socket_scan, Duration::from_secs(5), Duration::from_secs(1)) liveness_scan_with_waits(&sidecar_scan, &Endpoint::Unix(socket_scan), Duration::from_secs(5), Duration::from_secs(1))
}); });
// 主线程在窗口内删 sidecar(模拟旧 server 退出序列),扫描应判无操作。 // 主线程在窗口内删 sidecar(模拟旧 server 退出序列),扫描应判无操作。
std::thread::sleep(Duration::from_millis(200)); std::thread::sleep(Duration::from_millis(200));
@@ -366,7 +381,7 @@ mod tests {
sidecar::write_exclusive(&id, &sidecar).expect("写 sidecar 应成功"); sidecar::write_exclusive(&id, &sidecar).expect("写 sidecar 应成功");
// 注入短僵尸退出窗口(500 毫秒)加短杀进程等待(3 秒):子进程存活, // 注入短僵尸退出窗口(500 毫秒)加短杀进程等待(3 秒):子进程存活,
// sidecar 不消失,扫描等窗口后杀进程。 // sidecar 不消失,扫描等窗口后杀进程。
let outcome = liveness_scan_with_waits(&sidecar, &socket, Duration::from_millis(500), Duration::from_secs(3)); let outcome = liveness_scan_with_waits(&sidecar, &Endpoint::Unix(socket.clone()), Duration::from_millis(500), Duration::from_secs(3));
assert_eq!(outcome, LivenessOutcome::Cleaned, "刻度匹配的存活进程应被杀且文件被删"); assert_eq!(outcome, LivenessOutcome::Cleaned, "刻度匹配的存活进程应被杀且文件被删");
// 有界等待回收线程:子进程被杀后收尸即退出,不无限阻塞。 // 有界等待回收线程:子进程被杀后收尸即退出,不无限阻塞。
let deadline = Instant::now() + Duration::from_secs(5); let deadline = Instant::now() + Duration::from_secs(5);
@@ -396,7 +411,7 @@ mod tests {
// 注入短僵尸退出窗口(500 毫秒)加短杀进程等待(300 毫秒):子进程忽略 // 注入短僵尸退出窗口(500 毫秒)加短杀进程等待(300 毫秒):子进程忽略
// SIGTERM,扫描判杀进程失败,文件原样。 // SIGTERM,扫描判杀进程失败,文件原样。
let outcome = let outcome =
liveness_scan_with_waits(&sidecar, &socket, Duration::from_millis(500), Duration::from_millis(300)); liveness_scan_with_waits(&sidecar, &Endpoint::Unix(socket.clone()), Duration::from_millis(500), Duration::from_millis(300));
assert_eq!(outcome, LivenessOutcome::KillFailed, "忽略 SIGTERM 应判杀进程失败"); assert_eq!(outcome, LivenessOutcome::KillFailed, "忽略 SIGTERM 应判杀进程失败");
assert!(sidecar.exists(), "sidecar 应原样"); assert!(sidecar.exists(), "sidecar 应原样");
assert!(!socket.exists(), "socket 文件应保持不存在"); assert!(!socket.exists(), "socket 文件应保持不存在");
+40 -63
View File
@@ -89,11 +89,11 @@ pub struct ServerConfig {
} }
impl ServerConfig { impl ServerConfig {
/// 生产默认配置:endpoint 与 sidecar 取 well-known 位置,worker 数取宿主 /// 生产默认配置:endpoint 取 well-known TCP 端口,sidecar 取 well-known
/// 核数,版本取构建期版本。 /// 位置,worker 数取宿主核数,版本取构建期版本。
pub fn well_known() -> Self { pub fn well_known() -> Self {
ServerConfig { ServerConfig {
endpoint: Endpoint::Unix(sidecar::well_known_socket_path()), endpoint: sidecar::well_known_endpoint(),
sidecar_path: sidecar::well_known_sidecar_path(), sidecar_path: sidecar::well_known_sidecar_path(),
worker_count: default_worker_count(), worker_count: default_worker_count(),
version: env!("CARGO_PKG_VERSION").to_string(), version: env!("CARGO_PKG_VERSION").to_string(),
@@ -207,27 +207,23 @@ pub fn run_server(config: &ServerConfig) -> Result<(), ServerError> {
levels: [Level::Info; SUBSYSTEM_COUNT], levels: [Level::Info; SUBSYSTEM_COUNT],
}); });
// liveness 扫描、sidecar 认领与交接协议都在 Unix 文件面上;TCP 臂跳过 // 启动时 liveness 扫描:认领裁决前清掉陈旧残留(僵尸态自动恢复)。
// sidecar 注册,同端口裁决交给绑定失败。 // 杀进程失败映射既有的 ClaimRefused(RefusedAmbiguous),诊断指向人工
if let Endpoint::Unix(socket_path) = &config.endpoint { // 清理命令。两臂通用:TCP server 也写 sidecar,同端口裁决交给绑定失败。
// 启动时 liveness 扫描:认领裁决前清掉陈旧残留(僵尸态自动恢复)。 match liveness::liveness_scan(&config.sidecar_path, &config.endpoint) {
// 杀进程失败映射既有的 ClaimRefused(RefusedAmbiguous),诊断指向人工 liveness::LivenessOutcome::Noop | liveness::LivenessOutcome::Cleaned => {}
// 清理命令。 liveness::LivenessOutcome::KillFailed => {
match liveness::liveness_scan(&config.sidecar_path, socket_path) { return Err(ServerError::ClaimRefused(ClaimResult::RefusedAmbiguous))
liveness::LivenessOutcome::Noop | liveness::LivenessOutcome::Cleaned => {}
liveness::LivenessOutcome::KillFailed => {
return Err(ServerError::ClaimRefused(ClaimResult::RefusedAmbiguous))
}
} }
}
// 认领 sidecar(启动顺序契约:先写 sidecar 再绑定监听器):同版本 // 认领 sidecar(启动顺序契约:先写 sidecar 再绑定监听器):同版本
// 拒绝启动,异版本走交接路径,存活证据不足即返回错误。 // 拒绝启动,异版本走交接路径,存活证据不足即返回错误。
match sidecar::claim_sidecar(&config.sidecar_path, socket_path, &config.version) { match sidecar::claim_sidecar(&config.sidecar_path, &config.endpoint, &config.version) {
ClaimResult::Registered => {} ClaimResult::Registered => {}
ClaimResult::AlreadyRunning => return Err(ServerError::ClaimRefused(ClaimResult::AlreadyRunning)), ClaimResult::AlreadyRunning => return Err(ServerError::ClaimRefused(ClaimResult::AlreadyRunning)),
ClaimResult::VersionMismatch => yield_and_takeover(config)?, ClaimResult::VersionMismatch => yield_and_takeover(config)?,
ClaimResult::RefusedAmbiguous => return Err(ServerError::ClaimRefused(ClaimResult::RefusedAmbiguous)), ClaimResult::RefusedAmbiguous => return Err(ServerError::ClaimRefused(ClaimResult::RefusedAmbiguous)),
}
} }
// 关闭标志提前创建并注册信号处理器:在绑定 socket 之前,socket 文件 // 关闭标志提前创建并注册信号处理器:在绑定 socket 之前,socket 文件
@@ -460,18 +456,15 @@ fn finish_shutdown(
/// 2. 设 5 秒读超时,写 yield 请求帧。 /// 2. 设 5 秒读超时,写 yield 请求帧。
/// 3. 读一帧应答:读超时(旧 server 挂起)或应答非 yield 确认(旧版本 /// 3. 读一帧应答:读超时(旧 server 挂起)或应答非 yield 确认(旧版本
/// binary 无该变体时以错误帧答复未知类型)判 `YieldFailed`。 /// binary 无该变体时以错误帧答复未知类型)判 `YieldFailed`。
/// 4. 关闭连接,按 endpoint 形态等旧 server 退出:Unix 臂轮询 sidecar 文件 /// 4. 关闭连接,轮询 sidecar 文件消失(50 毫秒间隔、10 秒上限)判旧 server
/// 消失,TCP 臂以端口释放(连接被拒)为退出信号;均 50 毫秒间隔、 /// 退出;超时判 `YieldFailed`。
/// 10 秒上限,超时判 `YieldFailed`。 /// 5. 重新认领 sidecar;得 `Registered` 即接管成功,其余态判 `YieldFailed`。
/// 5. Unix 臂重新认领 sidecar;得 `Registered` 即接管成功,其余态判
/// `YieldFailed`。TCP 臂无 sidecar 文件,不重认领。
/// ///
/// # 边界与前提 /// # 边界与前提
/// - 交接等待 10 秒长于旧 server 的排水安全超时 5 秒,正常路径必然先于 /// - 交接等待 10 秒长于旧 server 的排水安全超时 5 秒,正常路径必然先于
/// 等待上限完成。 /// 等待上限完成。
/// - Unix 臂:旧 server 先删 socket 文件后删 sidecar,sidecar 消失时 /// - 两臂统一以 sidecar 消失为退出信号:旧 server 先删 socket 文件(Unix
/// socket 文件必已删除,接管绑定无竞态。 /// 臂)后删 sidecar,sidecar 消失时接管绑定无竞态。
/// - TCP 臂:监听者 drop 即端口释放,连接被拒即旧 server 已退出。
/// ///
/// # 副作用 /// # 副作用
/// - 失败时不触碰旧 server 的文件(socket 与 sidecar),人工兜底归清理命令。 /// - 失败时不触碰旧 server 的文件(socket 与 sidecar),人工兜底归清理命令。
@@ -479,7 +472,7 @@ fn finish_shutdown(
/// # 注意事项 /// # 注意事项
/// - 纯函数式入口:版本管理路径(版本不匹配强制交接)与显式交接路径调用 /// - 纯函数式入口:版本管理路径(版本不匹配强制交接)与显式交接路径调用
/// 同一入口。 /// 同一入口。
/// - 退出信号源按形态分:Unix 臂是 sidecar 文件消失,TCP 臂是端口释放。 /// - 退出信号源是 sidecar 文件消失(两臂同源,TCP server 也写 sidecar)。
pub fn yield_and_takeover(config: &ServerConfig) -> Result<(), ServerError> { pub fn yield_and_takeover(config: &ServerConfig) -> Result<(), ServerError> {
// 连接旧 server:连接成功是交接的前提,两臂按 endpoint 形态分支。 // 连接旧 server:连接成功是交接的前提,两臂按 endpoint 形态分支。
let mut stream = match &config.endpoint { let mut stream = match &config.endpoint {
@@ -511,36 +504,20 @@ pub fn yield_and_takeover(config: &ServerConfig) -> Result<(), ServerError> {
// 关闭连接:旧 server 已受理,交接进入退出等待阶段。 // 关闭连接:旧 server 已受理,交接进入退出等待阶段。
drop(reader); drop(reader);
match &config.endpoint { // 轮询 sidecar 消失(旧 server 的退出序列以 sidecar 最后删除收口),
// Unix 臂:轮询 sidecar 消失(旧 server 的退出序列以 sidecar 最后 // 消失后重新认领。两臂统一:TCP server 也写 sidecar,退出信号同源。
// 删除收口),消失后重新认领。 let deadline = Instant::now() + Duration::from_secs(10);
Endpoint::Unix(path) => { while config.sidecar_path.exists() {
let deadline = Instant::now() + Duration::from_secs(10); if Instant::now() >= deadline {
while config.sidecar_path.exists() { return Err(ServerError::YieldFailed("旧 server 10 秒内未退出,sidecar 文件未消失".into()));
if Instant::now() >= deadline { }
return Err(ServerError::YieldFailed("旧 server 10 秒内未退出,sidecar 文件未消失".into())); std::thread::sleep(Duration::from_millis(50));
} }
std::thread::sleep(Duration::from_millis(50));
}
// 重新认领:sidecar 已消失,O_EXCL 创建应成功;其余态判交接失败。 // 重新认领:sidecar 已消失,O_EXCL 创建应成功;其余态判交接失败。
match sidecar::claim_sidecar(&config.sidecar_path, path, &config.version) { match sidecar::claim_sidecar(&config.sidecar_path, &config.endpoint, &config.version) {
ClaimResult::Registered => Ok(()), ClaimResult::Registered => Ok(()),
other => Err(ServerError::YieldFailed(format!("交接后重新认领未通过:{other:?}"))), other => Err(ServerError::YieldFailed(format!("交接后重新认领未通过:{other:?}"))),
}
}
// TCP 臂:无 sidecar 文件可轮询,以端口释放(连接被拒)为旧 server
// 退出信号,10 秒上限,不重认领。
Endpoint::Tcp(addr) => {
let deadline = Instant::now() + Duration::from_secs(10);
while TcpStream::connect(*addr).is_ok() {
if Instant::now() >= deadline {
return Err(ServerError::YieldFailed("旧 server 10 秒内未退出,端口仍被占用".into()));
}
std::thread::sleep(Duration::from_millis(50));
}
Ok(())
}
} }
} }
@@ -1316,7 +1293,7 @@ mod tests {
// VersionMismatch)。 // VersionMismatch)。
let listener = UnixListener::bind(unix_path(&config.endpoint)).expect("绑定模拟旧 server 应成功"); let listener = UnixListener::bind(unix_path(&config.endpoint)).expect("绑定模拟旧 server 应成功");
sidecar::write_exclusive( sidecar::write_exclusive(
&sidecar::ServerIdentity::new(unix_path(&config.endpoint).to_str().unwrap(), "0.0.0"), &sidecar::ServerIdentity::new(&config.endpoint, "0.0.0"),
&config.sidecar_path, &config.sidecar_path,
) )
.expect("写模拟 sidecar 应成功"); .expect("写模拟 sidecar 应成功");
@@ -1349,7 +1326,7 @@ mod tests {
let listener = UnixListener::bind(unix_path(&config.endpoint)).expect("绑定模拟旧 server 应成功"); let listener = UnixListener::bind(unix_path(&config.endpoint)).expect("绑定模拟旧 server 应成功");
sidecar::write_exclusive( sidecar::write_exclusive(
&sidecar::ServerIdentity::new(unix_path(&config.endpoint).to_str().unwrap(), "0.0.0"), &sidecar::ServerIdentity::new(&config.endpoint, "0.0.0"),
&config.sidecar_path, &config.sidecar_path,
) )
.expect("写模拟 sidecar 应成功"); .expect("写模拟 sidecar 应成功");
@@ -1372,7 +1349,7 @@ mod tests {
} }
/// 交接 TCP 臂:假 server 回 yield 确认帧后 drop 监听器(端口释放), /// 交接 TCP 臂:假 server 回 yield 确认帧后 drop 监听器(端口释放),
/// 交接以端口释放为退出信号返回成功(不重认领)。 /// 无 sidecar 可轮询(轮询即跳过),重新认领 O_EXCL 创建成功,交接返回成功。
#[test] #[test]
fn yield_takeover_tcp_arm() { fn yield_takeover_tcp_arm() {
let listener = TcpListener::bind("127.0.0.1:0").expect("绑定假 server 应成功"); let listener = TcpListener::bind("127.0.0.1:0").expect("绑定假 server 应成功");
+109 -53
View File
@@ -13,7 +13,7 @@
use std::fs::{self, File, OpenOptions}; use std::fs::{self, File, OpenOptions};
use std::io::{self, Write}; use std::io::{self, Write};
use std::net::TcpStream; use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream};
use std::os::unix::io::AsRawFd; use std::os::unix::io::AsRawFd;
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -27,7 +27,8 @@ use serde::{Deserialize, Serialize};
/// ///
/// 字段: /// 字段:
/// - `pid`:server 进程标识符,清理命令杀进程用。 /// - `pid`:server 进程标识符,清理命令杀进程用。
/// - `socket_path`:well-known socket 的绝对路径,控制客户端连接用。 /// - `socket_path`:监听端点的文本形态(Unix 臂为路径、TCP 臂为 host:port),
/// 控制客户端连接用。
/// - `version`:构建期版本,版本交接时比对用。 /// - `version`:构建期版本,版本交接时比对用。
/// - `started_at`:Unix 秒,人读展示用。 /// - `started_at`:Unix 秒,人读展示用。
/// - `starttime`:`/proc/<pid>/stat` 的启动时刻刻度(自开机单调递增), /// - `starttime`:`/proc/<pid>/stat` 的启动时刻刻度(自开机单调递增),
@@ -47,14 +48,16 @@ impl ServerIdentity {
/// # 边界与前提 /// # 边界与前提
/// - 版本由调用方注入(生产默认取构建期版本,测试注入可区分值), /// - 版本由调用方注入(生产默认取构建期版本,测试注入可区分值),
/// 本函数不读取构建期版本。 /// 本函数不读取构建期版本。
/// - `socket_path` 字段记监听端点的文本形态(`endpoint.describe()`),
/// 字段名与 JSON 模式不变,旧 sidecar 可读。
/// - 非 Linux 宿主(`/proc` 缺位)时 `starttime` 填 0,交叉核对退化, /// - 非 Linux 宿主(`/proc` 缺位)时 `starttime` 填 0,交叉核对退化,
/// liveness 由 socket 探活裁决。 /// liveness 由 endpoint 探活裁决。
pub fn new(socket_path: impl AsRef<Path>, version: &str) -> Self { pub fn new(endpoint: &Endpoint, version: &str) -> Self {
let pid = std::process::id(); let pid = std::process::id();
let started_at = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0); let started_at = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
ServerIdentity { ServerIdentity {
pid, pid,
socket_path: socket_path.as_ref().to_string_lossy().into_owned(), socket_path: endpoint.describe(),
version: version.to_string(), version: version.to_string(),
started_at, started_at,
starttime: process_starttime(pid).unwrap_or(0), starttime: process_starttime(pid).unwrap_or(0),
@@ -81,16 +84,36 @@ pub fn current_uid() -> u32 {
0 0
} }
/// well-known TCP 端口:持久 server 缺省端口。
pub const WELL_KNOWN_PORT: u16 = 4242;
/// well-known sidecar 路径:`$TMPDIR/oxide-<uid>.json`。 /// well-known sidecar 路径:`$TMPDIR/oxide-<uid>.json`。
pub fn well_known_sidecar_path() -> PathBuf { pub fn well_known_sidecar_path() -> PathBuf {
std::env::temp_dir().join(format!("oxide-{}.json", current_uid())) std::env::temp_dir().join(format!("oxide-{}.json", current_uid()))
} }
/// well-known 持久 server socket 路径:`$TMPDIR/oxide-<uid>.sock`。 /// well-known 持久 server socket 路径:`$TMPDIR/oxide-<uid>.sock`。
///
/// 保留给 `--rm` 独立模式(进程唯一路径)与旧残留清理;持久 server 缺省
/// 端点已切到 well-known TCP 端口。
pub fn well_known_socket_path() -> PathBuf { pub fn well_known_socket_path() -> PathBuf {
std::env::temp_dir().join(format!("oxide-{}.sock", current_uid())) 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))
}
/// 解析端点文本形态:先试 `SocketAddr`(host:port)得 TCP 臂,失败回
/// Unix 臂(路径形态)。sidecar 读写的单一入口,解析放这里。
pub fn parse_endpoint(s: &str) -> Endpoint {
match s.parse::<SocketAddr>() {
Ok(addr) => Endpoint::Tcp(addr),
Err(_) => Endpoint::Unix(PathBuf::from(s)),
}
}
/// well-known 日志文件路径:`$TMPDIR/oxide-<uid>.log`(与 sidecar 同主名)。 /// well-known 日志文件路径:`$TMPDIR/oxide-<uid>.log`(与 sidecar 同主名)。
pub fn well_known_log_path() -> PathBuf { pub fn well_known_log_path() -> PathBuf {
well_known_sidecar_path().with_extension("log") well_known_sidecar_path().with_extension("log")
@@ -229,86 +252,98 @@ pub enum ClaimResult {
/// ///
/// # 步骤 /// # 步骤
/// 1. 尝试 O_EXCL 创建;sidecar 不存在即成功。 /// 1. 尝试 O_EXCL 创建;sidecar 不存在即成功。
/// 2. sidecar 已存在:探活 socket,连接成功即有存活 server,读回旧 sidecar /// 2. sidecar 已存在:先读回,探活 sidecar 记录的 endpoint(不可读时探
/// 调用方 endpoint 兜底),连接成功即有存活 server,读回旧 sidecar
/// 与本进程版本比对:匹配判 `AlreadyRunning`,不匹配或不可读判 /// 与本进程版本比对:匹配判 `AlreadyRunning`,不匹配或不可读判
/// `VersionMismatch`。 /// `VersionMismatch`。
/// 3. socket 死:按 sidecar 可读性分支。 /// 3. endpoint 死:按 sidecar 可读性分支。
/// - 可解析且 PID 死:陈旧残留,删 sidecar 与 socket 文件后重建。 /// - 可解析且 PID 死:陈旧残留,删 sidecar 与 socket 文件(Unix 臂)后重建。
/// - 可解析且 PID 活:交叉核对启动时刻刻度,匹配则保守拒绝(server /// - 可解析且 PID 活:交叉核对启动时刻刻度,匹配则保守拒绝(server
/// 可能正在启动或 socket 被外部删除),失配则 PID 已复用,删除重建。 /// 可能正在启动或 endpoint 被外部释放),失配则 PID 已复用,删除重建。
/// - 不可解析(损坏):删 sidecar 与 socket 文件后重建。 /// - 不可解析(损坏):删 sidecar 与 socket 文件(Unix 臂)后重建。
/// ///
/// # 边界与前提 /// # 边界与前提
/// - 启动顺序契约是「先写 sidecar 再 bind socket」,存活 server 必有 sidecar。 /// - 启动顺序契约是「先写 sidecar 再 bind endpoint」,存活 server 必有 sidecar。
/// - 探活必须探 sidecar 记录的 endpoint 而非调用方自己的 endpoint:端口
/// 不同时探自己会把存活 server 误判为死并破坏其 sidecar(双注册场景)。
/// - 版本比对是精确字符串相等(同一性判定,不是新旧次序判定);旧 sidecar /// - 版本比对是精确字符串相等(同一性判定,不是新旧次序判定);旧 sidecar
/// 不可读(损坏、缺 version 字段)按不匹配处理,方向安全(强制交接失败 /// 不可读(损坏、缺 version 字段)按不匹配处理,方向安全(强制交接失败
/// 不触碰旧 server 的文件,旧 server 继续运行)。 /// 不触碰旧 server 的文件,旧 server 继续运行)。
/// ///
/// # 副作用 /// # 副作用
/// - 可能创建、删除、重写 sidecar 与 socket 文件。 /// - 可能创建、删除、重写 sidecar 与 socket 文件(Unix 臂)。
/// ///
/// # 注意事项 /// # 注意事项
/// - 意外 io 错误(权限等)保守判 `RefusedAmbiguous`;方向安全(拒绝注册 /// - 意外 io 错误(权限等)保守判 `RefusedAmbiguous`;方向安全(拒绝注册
/// 不会双注册)。 /// 不会双注册)。
pub fn claim_sidecar(sidecar_path: &Path, socket_path: &Path, new_version: &str) -> ClaimResult { pub fn claim_sidecar(sidecar_path: &Path, endpoint: &Endpoint, new_version: &str) -> ClaimResult {
// 先尝试 O_EXCL 创建:sidecar 不存在即成功。 // 先尝试 O_EXCL 创建:sidecar 不存在即成功。
match write_exclusive(&ServerIdentity::new(socket_path, new_version), sidecar_path) { match write_exclusive(&ServerIdentity::new(endpoint, new_version), sidecar_path) {
Ok(()) => return ClaimResult::Registered, Ok(()) => return ClaimResult::Registered,
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {} Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {}
Err(_) => return ClaimResult::RefusedAmbiguous, Err(_) => return ClaimResult::RefusedAmbiguous,
} }
// socket 探活是存活的唯一事实源:连接成功即有存活 server。 // sidecar 已存在:先读回。探活目标是 sidecar 记录的 endpoint(正确性
if is_server_alive(&Endpoint::Unix(socket_path.to_path_buf())) { // 红线:探调用方自己的端口会在端口不同时把存活 server 误判为死);
// 不可读时探调用方 endpoint(损坏文件防御性兜底,探自己)。
let recorded = read_identity(sidecar_path);
let probe = match &recorded {
Some(id) => parse_endpoint(&id.socket_path),
None => endpoint.clone(),
};
if is_server_alive(&probe) {
// 读回旧 sidecar 与本进程版本比对:匹配拒绝启动,不匹配或不可读 // 读回旧 sidecar 与本进程版本比对:匹配拒绝启动,不匹配或不可读
// 交调用方强制交接。 // 交调用方强制交接。
match read_identity(sidecar_path) { match &recorded {
Some(id) if id.version == new_version => return ClaimResult::AlreadyRunning, Some(id) if id.version == new_version => return ClaimResult::AlreadyRunning,
_ => return ClaimResult::VersionMismatch, _ => return ClaimResult::VersionMismatch,
} }
} }
// socket 死:按 sidecar 可读性分支。 // endpoint 死:按 sidecar 可读性分支。
match read_identity(sidecar_path) { match recorded {
Some(id) => claim_dead_socket(&id, sidecar_path, socket_path, new_version), Some(id) => claim_dead_socket(&id, sidecar_path, endpoint, new_version),
None => { None => {
// 损坏文件:删 sidecar 与 socket 文件后重建(socket 探活已失败, // 损坏文件:删 sidecar 与 socket 文件(Unix 臂)后重建(endpoint
// 文件无监听者,不会误删存活 server 的 socket)。 // 探活已失败,文件无监听者,不会误删存活 server 的 socket)。
remove_stale(sidecar_path, socket_path); remove_stale(sidecar_path, endpoint);
re_register(sidecar_path, socket_path, new_version) re_register(sidecar_path, endpoint, new_version)
} }
} }
} }
/// socket 死且 sidecar 可解析:按 PID 与启动时刻刻度裁决陈旧性。 /// endpoint 死且 sidecar 可解析:按 PID 与启动时刻刻度裁决陈旧性。
fn claim_dead_socket(id: &ServerIdentity, sidecar_path: &Path, socket_path: &Path, new_version: &str) -> ClaimResult { fn claim_dead_socket(id: &ServerIdentity, sidecar_path: &Path, endpoint: &Endpoint, new_version: &str) -> ClaimResult {
// PID 死:陈旧残留,删 sidecar 与 socket 文件后重建。 // PID 死:陈旧残留,删 sidecar 与 socket 文件(Unix 臂)后重建。
if !pid_alive(id.pid) { if !pid_alive(id.pid) {
remove_stale(sidecar_path, socket_path); remove_stale(sidecar_path, endpoint);
return re_register(sidecar_path, socket_path, new_version); return re_register(sidecar_path, endpoint, new_version);
} }
// PID 活:交叉核对启动时刻刻度裁决 PID 复用。 // PID 活:交叉核对启动时刻刻度裁决 PID 复用。
if process_starttime(id.pid) == Some(id.starttime) { if process_starttime(id.pid) == Some(id.starttime) {
// 进程活且刻度匹配:server 可能正在启动或 socket 被外部删除,保守拒绝。 // 进程活且刻度匹配:server 可能正在启动或 endpoint 被外部释放,保守拒绝。
return ClaimResult::RefusedAmbiguous; return ClaimResult::RefusedAmbiguous;
} }
// 刻度失配:PID 已被复用,sidecar 是陈旧残留。 // 刻度失配:PID 已被复用,sidecar 是陈旧残留。
remove_stale(sidecar_path, socket_path); remove_stale(sidecar_path, endpoint);
re_register(sidecar_path, socket_path, new_version) re_register(sidecar_path, endpoint, new_version)
} }
/// 删除 sidecar 与 socket 文件(socket 探活已失败,文件无监听者,不会误删 /// 删除 sidecar 与 socket 文件(Unix 臂;endpoint 探活已失败,文件无监听者,
/// 存活 server 的 socket)。 /// 不会误删存活 server 的 socket)。TCP 臂无文件可删,只删 sidecar。
fn remove_stale(sidecar_path: &Path, socket_path: &Path) { fn remove_stale(sidecar_path: &Path, endpoint: &Endpoint) {
let _ = fs::remove_file(sidecar_path); let _ = fs::remove_file(sidecar_path);
let _ = fs::remove_file(socket_path); if let Some(path) = endpoint.socket_path() {
let _ = fs::remove_file(path);
}
} }
/// 清理后重建 sidecar,重建失败保守拒绝。 /// 清理后重建 sidecar,重建失败保守拒绝。
fn re_register(sidecar_path: &Path, socket_path: &Path, new_version: &str) -> ClaimResult { fn re_register(sidecar_path: &Path, endpoint: &Endpoint, new_version: &str) -> ClaimResult {
match write_exclusive(&ServerIdentity::new(socket_path, new_version), sidecar_path) { match write_exclusive(&ServerIdentity::new(endpoint, new_version), sidecar_path) {
Ok(()) => ClaimResult::Registered, Ok(()) => ClaimResult::Registered,
Err(_) => ClaimResult::RefusedAmbiguous, Err(_) => ClaimResult::RefusedAmbiguous,
} }
@@ -381,7 +416,7 @@ mod tests {
fn identity_roundtrip() { fn identity_roundtrip() {
let dir = TestDir::new(); let dir = TestDir::new();
let path = dir.path("sidecar.json"); let path = dir.path("sidecar.json");
let id = ServerIdentity::new("/tmp/oxide-test.sock", "0.0.0"); let id = ServerIdentity::new(&Endpoint::Unix(PathBuf::from("/tmp/oxide-test.sock")), "0.0.0");
write_exclusive(&id, &path).expect("写 sidecar 应成功"); write_exclusive(&id, &path).expect("写 sidecar 应成功");
let read = read_identity(&path).expect("读回 sidecar 应成功"); let read = read_identity(&path).expect("读回 sidecar 应成功");
assert_eq!(read, id, "五字段应逐字段一致"); assert_eq!(read, id, "五字段应逐字段一致");
@@ -392,19 +427,28 @@ mod tests {
fn write_exclusive_second_create_fails() { fn write_exclusive_second_create_fails() {
let dir = TestDir::new(); let dir = TestDir::new();
let path = dir.path("sidecar.json"); let path = dir.path("sidecar.json");
let id = ServerIdentity::new("/tmp/oxide-test.sock", "0.0.0"); let id = ServerIdentity::new(&Endpoint::Unix(PathBuf::from("/tmp/oxide-test.sock")), "0.0.0");
write_exclusive(&id, &path).expect("首次写入应成功"); write_exclusive(&id, &path).expect("首次写入应成功");
let err = write_exclusive(&id, &path).unwrap_err(); let err = write_exclusive(&id, &path).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::AlreadyExists, "二次 create_new 必得 AlreadyExists"); assert_eq!(err.kind(), io::ErrorKind::AlreadyExists, "二次 create_new 必得 AlreadyExists");
} }
/// 端点文本解析:host:port 得 TCP 臂,路径形态回 Unix 臂。
#[test]
fn parse_endpoint_two_forms() {
let tcp = parse_endpoint("127.0.0.1:4242");
assert_eq!(tcp, Endpoint::Tcp(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 4242)), "host:port 应得 TCP 臂");
let unix = parse_endpoint("/tmp/oxide-test.sock");
assert_eq!(unix, Endpoint::Unix(PathBuf::from("/tmp/oxide-test.sock")), "路径形态应得 Unix 臂");
}
/// 认领分支:无 sidecar 时注册成功,sidecar 是本进程身份。 /// 认领分支:无 sidecar 时注册成功,sidecar 是本进程身份。
#[test] #[test]
fn claim_no_sidecar_registers() { fn claim_no_sidecar_registers() {
let dir = TestDir::new(); let dir = TestDir::new();
let sidecar = dir.path("sidecar.json"); let sidecar = dir.path("sidecar.json");
let socket = dir.path("server.sock"); let socket = dir.path("server.sock");
assert_eq!(claim_sidecar(&sidecar, &socket, "0.0.0"), ClaimResult::Registered); assert_eq!(claim_sidecar(&sidecar, &Endpoint::Unix(socket.clone()), "0.0.0"), ClaimResult::Registered);
let id = read_identity(&sidecar).expect("sidecar 应已写入"); let id = read_identity(&sidecar).expect("sidecar 应已写入");
assert_eq!(id.pid, std::process::id()); assert_eq!(id.pid, std::process::id());
assert_eq!(id.socket_path, socket.to_str().unwrap()); assert_eq!(id.socket_path, socket.to_str().unwrap());
@@ -417,10 +461,10 @@ mod tests {
let dir = TestDir::new(); let dir = TestDir::new();
let sidecar = dir.path("sidecar.json"); let sidecar = dir.path("sidecar.json");
let socket = dir.path("server.sock"); let socket = dir.path("server.sock");
// 存活 server 必有 sidecar(启动顺序契约:先写 sidecar 再 bind socket)。 // 存活 server 必有 sidecar(启动顺序契约:先写 sidecar 再 bind endpoint)。
let listener = UnixListener::bind(&socket).expect("绑定监听者应成功"); let listener = UnixListener::bind(&socket).expect("绑定监听者应成功");
write_exclusive(&ServerIdentity::new(socket.to_str().unwrap(), "0.0.0"), &sidecar).expect("写 sidecar 应成功"); write_exclusive(&ServerIdentity::new(&Endpoint::Unix(socket.clone()), "0.0.0"), &sidecar).expect("写 sidecar 应成功");
assert_eq!(claim_sidecar(&sidecar, &socket, "0.0.0"), ClaimResult::AlreadyRunning); assert_eq!(claim_sidecar(&sidecar, &Endpoint::Unix(socket.clone()), "0.0.0"), ClaimResult::AlreadyRunning);
drop(listener); drop(listener);
} }
@@ -433,8 +477,8 @@ mod tests {
let socket = dir.path("server.sock"); let socket = dir.path("server.sock");
let listener = UnixListener::bind(&socket).expect("绑定监听者应成功"); let listener = UnixListener::bind(&socket).expect("绑定监听者应成功");
// 旧 sidecar 版本与本进程版本不同:比对判不匹配。 // 旧 sidecar 版本与本进程版本不同:比对判不匹配。
write_exclusive(&ServerIdentity::new(socket.to_str().unwrap(), "0.0.0"), &sidecar).expect("写 sidecar 应成功"); write_exclusive(&ServerIdentity::new(&Endpoint::Unix(socket.clone()), "0.0.0"), &sidecar).expect("写 sidecar 应成功");
assert_eq!(claim_sidecar(&sidecar, &socket, "0.0.1"), ClaimResult::VersionMismatch); assert_eq!(claim_sidecar(&sidecar, &Endpoint::Unix(socket.clone()), "0.0.1"), ClaimResult::VersionMismatch);
drop(listener); drop(listener);
} }
@@ -445,7 +489,7 @@ mod tests {
let sidecar = dir.path("sidecar.json"); let sidecar = dir.path("sidecar.json");
let socket = dir.path("server.sock"); let socket = dir.path("server.sock");
write_exclusive(&stale_identity(dead_pid(), &socket), &sidecar).expect("写陈旧 sidecar 应成功"); write_exclusive(&stale_identity(dead_pid(), &socket), &sidecar).expect("写陈旧 sidecar 应成功");
assert_eq!(claim_sidecar(&sidecar, &socket, "0.0.0"), ClaimResult::Registered); assert_eq!(claim_sidecar(&sidecar, &Endpoint::Unix(socket.clone()), "0.0.0"), ClaimResult::Registered);
let id = read_identity(&sidecar).expect("重建 sidecar 应可读"); let id = read_identity(&sidecar).expect("重建 sidecar 应可读");
assert_eq!(id.pid, std::process::id(), "重建 sidecar 应为本进程身份"); assert_eq!(id.pid, std::process::id(), "重建 sidecar 应为本进程身份");
} }
@@ -458,7 +502,7 @@ mod tests {
let socket = dir.path("server.sock"); let socket = dir.path("server.sock");
// PID 是本进程(存活),启动时刻刻度为占位值:PID 已复用。 // PID 是本进程(存活),启动时刻刻度为占位值:PID 已复用。
write_exclusive(&stale_identity(std::process::id(), &socket), &sidecar).expect("写陈旧 sidecar 应成功"); write_exclusive(&stale_identity(std::process::id(), &socket), &sidecar).expect("写陈旧 sidecar 应成功");
assert_eq!(claim_sidecar(&sidecar, &socket, "0.0.0"), ClaimResult::Registered); assert_eq!(claim_sidecar(&sidecar, &Endpoint::Unix(socket.clone()), "0.0.0"), ClaimResult::Registered);
} }
/// 认领分支:存活 PID 加匹配启动时刻保守拒绝。 /// 认领分支:存活 PID 加匹配启动时刻保守拒绝。
@@ -468,9 +512,9 @@ mod tests {
let sidecar = dir.path("sidecar.json"); let sidecar = dir.path("sidecar.json");
let socket = dir.path("server.sock"); let socket = dir.path("server.sock");
// PID 与启动时刻刻度都是本进程的:进程活且刻度匹配。 // PID 与启动时刻刻度都是本进程的:进程活且刻度匹配。
let id = ServerIdentity::new(socket.to_str().unwrap(), "0.0.0"); let id = ServerIdentity::new(&Endpoint::Unix(socket.clone()), "0.0.0");
write_exclusive(&id, &sidecar).expect("写 sidecar 应成功"); write_exclusive(&id, &sidecar).expect("写 sidecar 应成功");
assert_eq!(claim_sidecar(&sidecar, &socket, "0.0.0"), ClaimResult::RefusedAmbiguous); assert_eq!(claim_sidecar(&sidecar, &Endpoint::Unix(socket.clone()), "0.0.0"), ClaimResult::RefusedAmbiguous);
} }
/// 认领分支:损坏文件加死 socket 判陈旧,sidecar 重建。 /// 认领分支:损坏文件加死 socket 判陈旧,sidecar 重建。
@@ -481,7 +525,7 @@ mod tests {
let socket = dir.path("server.sock"); let socket = dir.path("server.sock");
// 损坏文件:半写 JSON。 // 损坏文件:半写 JSON。
fs::write(&sidecar, r#"{"pid":1,"socket_path":""#).expect("写损坏文件应成功"); fs::write(&sidecar, r#"{"pid":1,"socket_path":""#).expect("写损坏文件应成功");
assert_eq!(claim_sidecar(&sidecar, &socket, "0.0.0"), ClaimResult::Registered); assert_eq!(claim_sidecar(&sidecar, &Endpoint::Unix(socket.clone()), "0.0.0"), ClaimResult::Registered);
let id = read_identity(&sidecar).expect("重建 sidecar 应可读"); let id = read_identity(&sidecar).expect("重建 sidecar 应可读");
assert_eq!(id.pid, std::process::id()); assert_eq!(id.pid, std::process::id());
} }
@@ -496,7 +540,7 @@ mod tests {
fs::write(&sidecar, "not json at all").expect("写损坏文件应成功"); fs::write(&sidecar, "not json at all").expect("写损坏文件应成功");
// 陈旧 socket 文件:文件存在但无监听者。 // 陈旧 socket 文件:文件存在但无监听者。
fs::write(&socket, b"stale").expect("写陈旧 socket 文件应成功"); fs::write(&socket, b"stale").expect("写陈旧 socket 文件应成功");
assert_eq!(claim_sidecar(&sidecar, &socket, "0.0.0"), ClaimResult::Registered); assert_eq!(claim_sidecar(&sidecar, &Endpoint::Unix(socket.clone()), "0.0.0"), ClaimResult::Registered);
let id = read_identity(&sidecar).expect("重建 sidecar 应可读"); let id = read_identity(&sidecar).expect("重建 sidecar 应可读");
assert_eq!(id.pid, std::process::id(), "重建 sidecar 应为本进程身份"); assert_eq!(id.pid, std::process::id(), "重建 sidecar 应为本进程身份");
assert!(!socket.exists(), "陈旧 socket 文件应被删除"); assert!(!socket.exists(), "陈旧 socket 文件应被删除");
@@ -511,10 +555,22 @@ mod tests {
let socket = dir.path("server.sock"); let socket = dir.path("server.sock");
let listener = UnixListener::bind(&socket).expect("绑定监听者应成功"); let listener = UnixListener::bind(&socket).expect("绑定监听者应成功");
fs::write(&sidecar, "not json at all").expect("写损坏文件应成功"); fs::write(&sidecar, "not json at all").expect("写损坏文件应成功");
assert_eq!(claim_sidecar(&sidecar, &socket, "0.0.0"), ClaimResult::VersionMismatch); assert_eq!(claim_sidecar(&sidecar, &Endpoint::Unix(socket.clone()), "0.0.0"), ClaimResult::VersionMismatch);
drop(listener); drop(listener);
} }
/// 认领分支(TCP 臂):无 sidecar 时注册成功,sidecar 记 host:port 文本形态。
#[test]
fn claim_tcp_no_sidecar_registers() {
let dir = TestDir::new();
let sidecar = dir.path("sidecar.json");
let endpoint = Endpoint::Tcp(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0));
assert_eq!(claim_sidecar(&sidecar, &endpoint, "0.0.0"), ClaimResult::Registered);
let id = read_identity(&sidecar).expect("sidecar 应已写入");
assert_eq!(id.pid, std::process::id());
assert_eq!(id.socket_path, endpoint.describe(), "sidecar 应记 host:port 文本形态");
}
/// --rm 路径:两次调用路径不同,且文件名含本进程 PID 与 oxide-rm- 前缀。 /// --rm 路径:两次调用路径不同,且文件名含本进程 PID 与 oxide-rm- 前缀。
#[test] #[test]
fn rm_socket_path_unique_and_contains_pid() { fn rm_socket_path_unique_and_contains_pid() {
+6 -4
View File
@@ -10,23 +10,25 @@ use std::time::{Duration, Instant};
use super::client::send_control_request; use super::client::send_control_request;
use super::protocol::{ServerRequest, ServerResponse}; use super::protocol::{ServerRequest, ServerResponse};
/// spawn 脱离的前台 server 子进程(`server start --foreground`):标准流全空,不等待。 /// spawn 脱离的前台 server 子进程(`server start --foreground --port N`):
/// 标准流全空,不等待。
/// ///
/// # 步骤 /// # 步骤
/// 1. 取当前可执行文件路径。 /// 1. 取当前可执行文件路径。
/// 2. 构造命令 `server start --foreground` 加可选 `--workers N`。 /// 2. 构造命令 `server start --foreground --port N` 加可选 `--workers N`。
/// 3. 三个标准流置空并 spawn。 /// 3. 三个标准流置空并 spawn。
/// ///
/// # 边界与前提 /// # 边界与前提
/// - 必须带 `--foreground`:守护形态 spawn 的是前台 server 入口,不带该旗标会 /// - 必须带 `--foreground`:守护形态 spawn 的是前台 server 入口,不带该旗标会
/// 再次进入守护形态形成递归 spawn。 /// 再次进入守护形态形成递归 spawn。
/// - `port` 是 TCP 监听端口;缺省取 well-known 端口(调用方传入)。
/// ///
/// # 副作用 /// # 副作用
/// - 创建脱离的子进程,调用方退出后由 init 收领继续运行。 /// - 创建脱离的子进程,调用方退出后由 init 收领继续运行。
pub fn spawn_detached_server(workers: Option<u32>) -> std::io::Result<std::process::Child> { pub fn spawn_detached_server(workers: Option<u32>, port: u16) -> std::io::Result<std::process::Child> {
let exe = std::env::current_exe()?; let exe = std::env::current_exe()?;
let mut cmd = Command::new(exe); let mut cmd = Command::new(exe);
cmd.args(["server", "start", "--foreground"]); cmd.args(["server", "start", "--foreground", "--port", &port.to_string()]);
if let Some(n) = workers { if let Some(n) = workers {
cmd.args(["--workers", &n.to_string()]); cmd.args(["--workers", &n.to_string()]);
} }
+4 -3
View File
@@ -158,8 +158,9 @@ pub fn run_watchdog(workers: Option<u32>) -> ExitCode {
return ExitCode::FAILURE; return ExitCode::FAILURE;
} }
// 初始 spawn 与就绪探测:失败即退 1,不进入监控循环。 // 初始 spawn 与就绪探测:失败即退 1,不进入监控循环。watchdog 子进程用
let mut child = match spawn_detached_server(workers) { // well-known 端口(范围控制:不加 --port 旗标)。
let mut child = match spawn_detached_server(workers, sidecar::WELL_KNOWN_PORT) {
Ok(child) => child, Ok(child) => child,
Err(err) => { Err(err) => {
eprintln!("spawn server 进程失败:{err}"); eprintln!("spawn server 进程失败:{err}");
@@ -245,7 +246,7 @@ pub fn run_watchdog(workers: Option<u32>) -> ExitCode {
return ExitCode::FAILURE; return ExitCode::FAILURE;
} }
std::thread::sleep(CRASH_BACKOFF); std::thread::sleep(CRASH_BACKOFF);
match spawn_detached_server(workers) { match spawn_detached_server(workers, sidecar::WELL_KNOWN_PORT) {
Ok(new_child) => { Ok(new_child) => {
child = new_child; child = new_child;
child_exited = None; child_exited = None;
+37 -38
View File
@@ -7,7 +7,8 @@
//! 触碰全局路径的测试经双重锁(进程内互斥加跨进程文件锁)串行,避免相互 //! 触碰全局路径的测试经双重锁(进程内互斥加跨进程文件锁)串行,避免相互
//! 干扰(含与单测二进制的并行干扰)。 //! 干扰(含与单测二进制的并行干扰)。
use std::io::{BufRead, BufReader, Write}; use std::io::{BufRead, BufReader};
use std::net::TcpStream;
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
use std::process::{Command, Output}; use std::process::{Command, Output};
use std::sync::{Mutex, MutexGuard}; use std::sync::{Mutex, MutexGuard};
@@ -16,7 +17,7 @@ use std::time::{Duration, Instant};
use oxide_cli::server::client::send_control_request; use oxide_cli::server::client::send_control_request;
use oxide_cli::server::protocol::{self, FrameReader, ServerRequest, ServerResponse}; use oxide_cli::server::protocol::{self, FrameReader, ServerRequest, ServerResponse};
use oxide_cli::server::sidecar; use oxide_cli::server::sidecar;
use oxide_net::Endpoint; use oxide_net::{Conn, Endpoint};
/// well-known 全局路径进程内互斥锁:同测试二进制内触碰全局路径的测试串行 /// well-known 全局路径进程内互斥锁:同测试二进制内触碰全局路径的测试串行
/// 执行(fcntl 锁以进程为单位,不覆盖同进程多线程)。 /// 执行(fcntl 锁以进程为单位,不覆盖同进程多线程)。
@@ -85,7 +86,7 @@ fn wait_pid_dead(pid: u32, deadline: Duration) {
} }
/// 发送请求帧并读回一帧响应。 /// 发送请求帧并读回一帧响应。
fn send_and_recv(stream: &mut UnixStream, request: &ServerRequest) -> ServerResponse { fn send_and_recv(stream: &mut Conn, request: &ServerRequest) -> ServerResponse {
stream stream
.write_all(protocol::encode_request(request).as_bytes()) .write_all(protocol::encode_request(request).as_bytes())
.expect("写请求应成功"); .expect("写请求应成功");
@@ -94,6 +95,15 @@ fn send_and_recv(stream: &mut UnixStream, request: &ServerRequest) -> ServerResp
protocol::parse_response(&frame).expect("响应帧解析应成功") protocol::parse_response(&frame).expect("响应帧解析应成功")
} }
/// 读 well-known sidecar 取 endpoint 并连接 server(两臂按形态分支)。
fn connect_well_known() -> Conn {
let id = sidecar::read_identity(&sidecar::well_known_sidecar_path()).expect("读 sidecar 应成功");
match sidecar::parse_endpoint(&id.socket_path) {
Endpoint::Unix(path) => Conn::Unix(UnixStream::connect(&path).expect("连接 server 应成功")),
Endpoint::Tcp(addr) => Conn::Tcp(TcpStream::connect(addr).expect("连接 server 应成功")),
}
}
/// help 列全部十一个子命令。 /// help 列全部十一个子命令。
#[test] #[test]
fn server_help_lists_subcommands() { fn server_help_lists_subcommands() {
@@ -115,7 +125,7 @@ fn server_help_lists_subcommands() {
fn server_log_no_file_exit_1() { fn server_log_no_file_exit_1() {
let _guard = lock_well_known(); let _guard = lock_well_known();
assert!( assert!(
!sidecar::is_server_alive(&Endpoint::Unix(sidecar::well_known_socket_path())), !sidecar::is_server_alive(&sidecar::well_known_endpoint()),
"存活 server 占用 well-known 路径,测试不抢占" "存活 server 占用 well-known 路径,测试不抢占"
); );
@@ -135,7 +145,7 @@ fn server_log_no_file_exit_1() {
fn server_control_arms_no_server_exit_1() { fn server_control_arms_no_server_exit_1() {
let _guard = lock_well_known(); let _guard = lock_well_known();
assert!( assert!(
!sidecar::is_server_alive(&Endpoint::Unix(sidecar::well_known_socket_path())), !sidecar::is_server_alive(&sidecar::well_known_endpoint()),
"存活 server 占用 well-known 路径,测试不抢占" "存活 server 占用 well-known 路径,测试不抢占"
); );
@@ -154,7 +164,7 @@ fn server_control_arms_no_server_exit_1() {
fn server_forge_no_server_exit_1() { fn server_forge_no_server_exit_1() {
let _guard = lock_well_known(); let _guard = lock_well_known();
assert!( assert!(
!sidecar::is_server_alive(&Endpoint::Unix(sidecar::well_known_socket_path())), !sidecar::is_server_alive(&sidecar::well_known_endpoint()),
"存活 server 占用 well-known 路径,测试不抢占" "存活 server 占用 well-known 路径,测试不抢占"
); );
@@ -169,7 +179,7 @@ fn server_forge_no_server_exit_1() {
fn server_cleanup_exit_0() { fn server_cleanup_exit_0() {
let _guard = lock_well_known(); let _guard = lock_well_known();
assert!( assert!(
!sidecar::is_server_alive(&Endpoint::Unix(sidecar::well_known_socket_path())), !sidecar::is_server_alive(&sidecar::well_known_endpoint()),
"存活 server 占用 well-known 路径,测试不抢占" "存活 server 占用 well-known 路径,测试不抢占"
); );
@@ -185,12 +195,11 @@ fn server_cleanup_exit_0() {
#[test] #[test]
fn server_start_e2e() { fn server_start_e2e() {
let _guard = lock_well_known(); let _guard = lock_well_known();
let socket = sidecar::well_known_socket_path();
let sidecar_path = sidecar::well_known_sidecar_path(); let sidecar_path = sidecar::well_known_sidecar_path();
// 探活先行:存活 server 占用全局路径时 panic,不抢占。 // 探活先行:存活 server 占用全局路径时 panic,不抢占。
assert!( assert!(
!sidecar::is_server_alive(&Endpoint::Unix(socket.clone())), !sidecar::is_server_alive(&sidecar::well_known_endpoint()),
"存活 server 占用 well-known 路径,测试不抢占" "存活 server 占用 well-known 路径,测试不抢占"
); );
@@ -205,7 +214,7 @@ fn server_start_e2e() {
let id = sidecar::read_identity(&sidecar_path).expect("读 sidecar 应成功"); let id = sidecar::read_identity(&sidecar_path).expect("读 sidecar 应成功");
// 版本请求帧:版本应等于构建期版本。 // 版本请求帧:版本应等于构建期版本。
let mut stream = UnixStream::connect(&socket).expect("连接 server 应成功"); let mut stream = connect_well_known();
let response = send_and_recv(&mut stream, &ServerRequest::Version); let response = send_and_recv(&mut stream, &ServerRequest::Version);
match response { match response {
ServerResponse::Version { version } => { ServerResponse::Version { version } => {
@@ -226,7 +235,6 @@ fn server_start_e2e() {
std::thread::sleep(Duration::from_millis(50)); std::thread::sleep(Duration::from_millis(50));
} }
assert!(!socket.exists(), "socket 文件应被删除");
assert!(!sidecar_path.exists(), "sidecar 文件应被删除"); assert!(!sidecar_path.exists(), "sidecar 文件应被删除");
wait_pid_dead(id.pid, Duration::from_secs(30)); wait_pid_dead(id.pid, Duration::from_secs(30));
} }
@@ -237,12 +245,11 @@ fn server_start_e2e() {
#[test] #[test]
fn server_start_when_running() { fn server_start_when_running() {
let _guard = lock_well_known(); let _guard = lock_well_known();
let socket = sidecar::well_known_socket_path();
let sidecar_path = sidecar::well_known_sidecar_path(); let sidecar_path = sidecar::well_known_sidecar_path();
// 探活先行:存活 server 占用全局路径时 panic,不抢占。 // 探活先行:存活 server 占用全局路径时 panic,不抢占。
assert!( assert!(
!sidecar::is_server_alive(&Endpoint::Unix(socket.clone())), !sidecar::is_server_alive(&sidecar::well_known_endpoint()),
"存活 server 占用 well-known 路径,测试不抢占" "存活 server 占用 well-known 路径,测试不抢占"
); );
@@ -259,7 +266,7 @@ fn server_start_when_running() {
assert!(stdout.contains("已启动"), "二次 start 应打印「已启动」:{stdout}"); assert!(stdout.contains("已启动"), "二次 start 应打印「已启动」:{stdout}");
// 收尾:发关闭请求,等 sidecar 消失。 // 收尾:发关闭请求,等 sidecar 消失。
let mut stream = UnixStream::connect(&socket).expect("连接 server 应成功"); let mut stream = connect_well_known();
let response = send_and_recv(&mut stream, &ServerRequest::Shutdown); let response = send_and_recv(&mut stream, &ServerRequest::Shutdown);
assert!(matches!(response, ServerResponse::Shutdown), "应得关闭确认帧:{response:?}"); assert!(matches!(response, ServerResponse::Shutdown), "应得关闭确认帧:{response:?}");
drop(stream); drop(stream);
@@ -268,7 +275,6 @@ fn server_start_when_running() {
assert!(Instant::now() < deadline, "server 应在有界时间内退出"); assert!(Instant::now() < deadline, "server 应在有界时间内退出");
std::thread::sleep(Duration::from_millis(50)); std::thread::sleep(Duration::from_millis(50));
} }
assert!(!socket.exists(), "socket 文件应被删除");
assert!(!sidecar_path.exists(), "sidecar 文件应被删除"); assert!(!sidecar_path.exists(), "sidecar 文件应被删除");
} }
@@ -278,23 +284,22 @@ fn server_start_when_running() {
#[test] #[test]
fn server_restart_e2e() { fn server_restart_e2e() {
let _guard = lock_well_known(); let _guard = lock_well_known();
let socket = sidecar::well_known_socket_path();
let sidecar_path = sidecar::well_known_sidecar_path(); let sidecar_path = sidecar::well_known_sidecar_path();
// 探活先行:存活 server 占用全局路径时 panic,不抢占。 // 探活先行:存活 server 占用全局路径时 panic,不抢占。
assert!( assert!(
!sidecar::is_server_alive(&Endpoint::Unix(socket.clone())), !sidecar::is_server_alive(&sidecar::well_known_endpoint()),
"存活 server 占用 well-known 路径,测试不抢占" "存活 server 占用 well-known 路径,测试不抢占"
); );
// 起子进程 A:worker 数压到 2,轮询 socket 文件出现(10 秒截止)判就绪。 // 起子进程 A:worker 数压到 2,轮询 sidecar 出现(10 秒截止)判就绪。
let mut child = Command::new(env!("CARGO_BIN_EXE_oxide")) let mut child = Command::new(env!("CARGO_BIN_EXE_oxide"))
.args(["server", "start", "--workers", "2"]) .args(["server", "start", "--workers", "2"])
.spawn() .spawn()
.expect("oxide server start 应可启动"); .expect("oxide server start 应可启动");
let deadline = Instant::now() + Duration::from_secs(10); let deadline = Instant::now() + Duration::from_secs(10);
while !socket.exists() { while sidecar::read_identity(&sidecar_path).is_none() {
assert!(Instant::now() < deadline, "socket 文件未出现"); assert!(Instant::now() < deadline, "sidecar 未出现");
std::thread::sleep(Duration::from_millis(10)); std::thread::sleep(Duration::from_millis(10));
} }
@@ -329,7 +334,7 @@ fn server_restart_e2e() {
assert_eq!(new_id.version, env!("CARGO_PKG_VERSION"), "新 sidecar 版本应与构建期版本一致"); assert_eq!(new_id.version, env!("CARGO_PKG_VERSION"), "新 sidecar 版本应与构建期版本一致");
// 连接新 server:健康请求得 healthy,版本请求得构建期版本。 // 连接新 server:健康请求得 healthy,版本请求得构建期版本。
let mut stream = UnixStream::connect(&socket).expect("连接新 server 应成功"); let mut stream = connect_well_known();
match send_and_recv(&mut stream, &ServerRequest::Health) { match send_and_recv(&mut stream, &ServerRequest::Health) {
ServerResponse::Health { healthy } => assert!(healthy, "新 server 应健康"), ServerResponse::Health { healthy } => assert!(healthy, "新 server 应健康"),
other => panic!("应得 Health 帧,实得 {other:?}"), other => panic!("应得 Health 帧,实得 {other:?}"),
@@ -341,7 +346,7 @@ fn server_restart_e2e() {
other => panic!("应得 Version 帧,实得 {other:?}"), other => panic!("应得 Version 帧,实得 {other:?}"),
} }
// 收尾:发关闭请求,等新 server 退出,断言 socket 与 sidecar 文件被删。 // 收尾:发关闭请求,等新 server 退出,断言 sidecar 文件被删。
let response = send_and_recv(&mut stream, &ServerRequest::Shutdown); let response = send_and_recv(&mut stream, &ServerRequest::Shutdown);
assert!(matches!(response, ServerResponse::Shutdown), "应得关闭确认帧:{response:?}"); assert!(matches!(response, ServerResponse::Shutdown), "应得关闭确认帧:{response:?}");
drop(stream); drop(stream);
@@ -350,7 +355,6 @@ fn server_restart_e2e() {
assert!(Instant::now() < deadline, "新 server 应在有界时间内退出"); assert!(Instant::now() < deadline, "新 server 应在有界时间内退出");
std::thread::sleep(Duration::from_millis(50)); std::thread::sleep(Duration::from_millis(50));
} }
assert!(!socket.exists(), "socket 文件应被删除");
assert!(!sidecar_path.exists(), "sidecar 文件应被删除"); assert!(!sidecar_path.exists(), "sidecar 文件应被删除");
} }
@@ -361,13 +365,12 @@ fn server_restart_e2e() {
#[test] #[test]
fn server_log_e2e() { fn server_log_e2e() {
let _guard = lock_well_known(); let _guard = lock_well_known();
let socket = sidecar::well_known_socket_path();
let sidecar_path = sidecar::well_known_sidecar_path(); let sidecar_path = sidecar::well_known_sidecar_path();
let log_path = sidecar::well_known_log_path(); let log_path = sidecar::well_known_log_path();
// 探活先行:存活 server 占用全局路径时 panic,不抢占。 // 探活先行:存活 server 占用全局路径时 panic,不抢占。
assert!( assert!(
!sidecar::is_server_alive(&Endpoint::Unix(socket.clone())), !sidecar::is_server_alive(&sidecar::well_known_endpoint()),
"存活 server 占用 well-known 路径,测试不抢占" "存活 server 占用 well-known 路径,测试不抢占"
); );
@@ -419,7 +422,7 @@ fn server_log_e2e() {
); );
// 关闭请求帧:读回关闭确认帧。 // 关闭请求帧:读回关闭确认帧。
let mut stream = UnixStream::connect(&socket).expect("连接 server 应成功"); let mut stream = connect_well_known();
let response = send_and_recv(&mut stream, &ServerRequest::Shutdown); let response = send_and_recv(&mut stream, &ServerRequest::Shutdown);
assert!(matches!(response, ServerResponse::Shutdown), "应得关闭确认帧:{response:?}"); assert!(matches!(response, ServerResponse::Shutdown), "应得关闭确认帧:{response:?}");
drop(stream); drop(stream);
@@ -445,22 +448,21 @@ fn server_log_e2e() {
#[test] #[test]
fn server_forge_e2e() { fn server_forge_e2e() {
let _guard = lock_well_known(); let _guard = lock_well_known();
let socket = sidecar::well_known_socket_path();
let sidecar_path = sidecar::well_known_sidecar_path(); let sidecar_path = sidecar::well_known_sidecar_path();
// 探活先行:存活 server 占用全局路径时 panic,不抢占。 // 探活先行:存活 server 占用全局路径时 panic,不抢占。
assert!( assert!(
!sidecar::is_server_alive(&Endpoint::Unix(socket.clone())), !sidecar::is_server_alive(&sidecar::well_known_endpoint()),
"存活 server 占用 well-known 路径,测试不抢占" "存活 server 占用 well-known 路径,测试不抢占"
); );
// start 为守护形态:CLI 即刻返回,server 是脱离的孙进程;轮询 socket 文件 // start 为守护形态:CLI 即刻返回,server 是脱离的孙进程;轮询 sidecar
// 出现判就绪(worker 数压到 2 避免按宿主核数预热的成本)。 // 出现判就绪(worker 数压到 2 避免按宿主核数预热的成本)。
let output = oxide(&["server", "start", "--workers", "2"]); let output = oxide(&["server", "start", "--workers", "2"]);
assert_eq!(output.status.code(), Some(0), "start 应退 0"); assert_eq!(output.status.code(), Some(0), "start 应退 0");
let deadline = Instant::now() + Duration::from_secs(10); let deadline = Instant::now() + Duration::from_secs(10);
while !socket.exists() { while sidecar::read_identity(&sidecar_path).is_none() {
assert!(Instant::now() < deadline, "socket 文件未出现"); assert!(Instant::now() < deadline, "sidecar 未出现");
std::thread::sleep(Duration::from_millis(10)); std::thread::sleep(Duration::from_millis(10));
} }
@@ -485,7 +487,7 @@ fn server_forge_e2e() {
assert!(stdout.contains("cache cleared"), "forge --clear-cache 应打印清缓存行:{stdout}"); assert!(stdout.contains("cache cleared"), "forge --clear-cache 应打印清缓存行:{stdout}");
// 关闭请求帧:读回关闭确认帧。 // 关闭请求帧:读回关闭确认帧。
let mut stream = UnixStream::connect(&socket).expect("连接 server 应成功"); let mut stream = connect_well_known();
let response = send_and_recv(&mut stream, &ServerRequest::Shutdown); let response = send_and_recv(&mut stream, &ServerRequest::Shutdown);
assert!(matches!(response, ServerResponse::Shutdown), "应得关闭确认帧:{response:?}"); assert!(matches!(response, ServerResponse::Shutdown), "应得关闭确认帧:{response:?}");
drop(stream); drop(stream);
@@ -497,7 +499,6 @@ fn server_forge_e2e() {
std::thread::sleep(Duration::from_millis(50)); std::thread::sleep(Duration::from_millis(50));
} }
assert!(!socket.exists(), "socket 文件应被删除");
assert!(!sidecar_path.exists(), "sidecar 文件应被删除"); assert!(!sidecar_path.exists(), "sidecar 文件应被删除");
} }
@@ -507,12 +508,11 @@ fn server_forge_e2e() {
#[test] #[test]
fn server_watchdog_e2e() { fn server_watchdog_e2e() {
let _guard = lock_well_known(); let _guard = lock_well_known();
let socket = sidecar::well_known_socket_path();
let sidecar_path = sidecar::well_known_sidecar_path(); let sidecar_path = sidecar::well_known_sidecar_path();
// 探活先行:存活 server 占用全局路径时 panic,不抢占。 // 探活先行:存活 server 占用全局路径时 panic,不抢占。
assert!( assert!(
!sidecar::is_server_alive(&Endpoint::Unix(socket.clone())), !sidecar::is_server_alive(&sidecar::well_known_endpoint()),
"存活 server 占用 well-known 路径,测试不抢占" "存活 server 占用 well-known 路径,测试不抢占"
); );
@@ -582,7 +582,7 @@ fn server_watchdog_e2e() {
.expect("watchdog 应在有界时间内完成重启探测"); .expect("watchdog 应在有界时间内完成重启探测");
// 连接新 server:健康请求得 healthy。 // 连接新 server:健康请求得 healthy。
let mut stream = UnixStream::connect(&socket).expect("连接新 server 应成功"); let mut stream = connect_well_known();
match send_and_recv(&mut stream, &ServerRequest::Health) { match send_and_recv(&mut stream, &ServerRequest::Health) {
ServerResponse::Health { healthy } => assert!(healthy, "新 server 应健康"), ServerResponse::Health { healthy } => assert!(healthy, "新 server 应健康"),
other => panic!("应得 Health 帧,实得 {other:?}"), other => panic!("应得 Health 帧,实得 {other:?}"),
@@ -613,12 +613,11 @@ fn server_watchdog_e2e() {
#[test] #[test]
fn server_watchdog_sigint_stops_server() { fn server_watchdog_sigint_stops_server() {
let _guard = lock_well_known(); let _guard = lock_well_known();
let socket = sidecar::well_known_socket_path();
let sidecar_path = sidecar::well_known_sidecar_path(); let sidecar_path = sidecar::well_known_sidecar_path();
// 探活先行:存活 server 占用全局路径时 panic,不抢占。 // 探活先行:存活 server 占用全局路径时 panic,不抢占。
assert!( assert!(
!sidecar::is_server_alive(&Endpoint::Unix(socket.clone())), !sidecar::is_server_alive(&sidecar::well_known_endpoint()),
"存活 server 占用 well-known 路径,测试不抢占" "存活 server 占用 well-known 路径,测试不抢占"
); );
+64
View File
@@ -99,3 +99,67 @@ fn control_client_end_to_end() {
); );
assert!(!config.sidecar_path.exists(), "sidecar 文件应被删除"); assert!(!config.sidecar_path.exists(), "sidecar 文件应被删除");
} }
/// TCP 臂端到端:bind 127.0.0.1:0 取空闲端口、drop 监听器、以该端口起 server,
/// 经控制客户端走五类请求加关闭,sidecar 记 host:port 文本形态且被删。
#[test]
fn control_client_tcp_arm_end_to_end() {
// bind 127.0.0.1:0 取空闲端口,drop 监听器释放端口。
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("取空闲端口应成功");
let addr = listener.local_addr().expect("取端口应成功");
drop(listener);
// TCP 配置:临时 sidecar 路径、worker 数 2、版本取构建期版本。
let dir = std::env::temp_dir().join(format!("oxide_server_client_tcp_{}_{}", std::process::id(), "tcp"));
fs::create_dir_all(&dir).expect("测试目录创建应成功");
let config = ServerConfig {
endpoint: Endpoint::Tcp(addr),
sidecar_path: dir.join("sidecar.json"),
worker_count: 2,
version: env!("CARGO_PKG_VERSION").to_string(),
};
// 后台线程起真实 run_server,等 sidecar 出现(TCP 臂无 socket 文件,
// sidecar 是就绪信号)。
let server_config = config.clone();
let handle = std::thread::spawn(move || run_server(&server_config));
let deadline = Instant::now() + Duration::from_secs(10);
while oxide_cli::server::sidecar::read_identity(&config.sidecar_path).is_none() {
assert!(Instant::now() < deadline, "sidecar 未出现");
std::thread::sleep(Duration::from_millis(10));
}
// 版本请求:版本与构建期版本一致。
let response = send_control_request_from(&config.sidecar_path, &ServerRequest::Version).expect("版本请求应成功");
match response {
ServerResponse::Version { version } => {
assert_eq!(version, env!("CARGO_PKG_VERSION"), "版本应与构建期版本一致");
}
other => panic!("应得 Version 帧,实得 {other:?}"),
}
// 健康请求:healthy 为真。
let response = send_control_request_from(&config.sidecar_path, &ServerRequest::Health).expect("健康请求应成功");
assert!(
matches!(response, ServerResponse::Health { healthy: true }),
"healthy 应为真:{response:?}"
);
// 信息请求:endpoint 值与配置一致(host:port 文本形态)、进程号为当前进程号。
let response = send_control_request_from(&config.sidecar_path, &ServerRequest::Info).expect("信息请求应成功");
match response {
ServerResponse::Info { socket_path, pid, .. } => {
assert_eq!(socket_path, config.endpoint.describe(), "endpoint 值应与配置一致");
assert_eq!(pid, std::process::id(), "进程号应为当前进程号");
}
other => panic!("应得 Info 帧,实得 {other:?}"),
}
// 关闭请求:得确认帧,server 线程正常退出,sidecar 文件被删(TCP 臂无
// socket 文件)。
let response = send_control_request_from(&config.sidecar_path, &ServerRequest::Shutdown).expect("关闭请求应成功");
assert!(matches!(response, ServerResponse::Shutdown), "应得关闭确认帧:{response:?}");
let result = handle.join().expect("server 线程应正常退出");
assert!(result.is_ok(), "server 应正常退出:{result:?}");
assert!(!config.sidecar_path.exists(), "sidecar 文件应被删除");
}