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