mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-26 19:51:11 +08:00
feat: server restart 子命令加五臂打印定稿与错误消息打磨
This commit is contained in:
Generated
+18
-18
@@ -1160,7 +1160,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxide_builtins"
|
name = "oxide_builtins"
|
||||||
version = "0.8.37"
|
version = "0.8.38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"fast-float",
|
"fast-float",
|
||||||
@@ -1185,7 +1185,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxide_bytecode"
|
name = "oxide_bytecode"
|
||||||
version = "0.8.37"
|
version = "0.8.38"
|
||||||
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.8.37"
|
version = "0.8.38"
|
||||||
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.8.37"
|
version = "0.8.38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ansi_term",
|
"ansi_term",
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
@@ -1225,7 +1225,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxide_code_cache"
|
name = "oxide_code_cache"
|
||||||
version = "0.8.37"
|
version = "0.8.38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lru",
|
"lru",
|
||||||
"oxide_bytecode",
|
"oxide_bytecode",
|
||||||
@@ -1234,7 +1234,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxide_compiler"
|
name = "oxide_compiler"
|
||||||
version = "0.8.37"
|
version = "0.8.38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"oxide_bytecode",
|
"oxide_bytecode",
|
||||||
"oxide_cfg",
|
"oxide_cfg",
|
||||||
@@ -1253,7 +1253,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxide_dce"
|
name = "oxide_dce"
|
||||||
version = "0.8.37"
|
version = "0.8.38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"oxide_bytecode",
|
"oxide_bytecode",
|
||||||
"oxide_cfg",
|
"oxide_cfg",
|
||||||
@@ -1267,7 +1267,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxide_emit"
|
name = "oxide_emit"
|
||||||
version = "0.8.37"
|
version = "0.8.38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-bigint",
|
"num-bigint",
|
||||||
"oxc_allocator",
|
"oxc_allocator",
|
||||||
@@ -1282,7 +1282,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxide_ir"
|
name = "oxide_ir"
|
||||||
version = "0.8.37"
|
version = "0.8.38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"oxide_bytecode",
|
"oxide_bytecode",
|
||||||
"oxide_log",
|
"oxide_log",
|
||||||
@@ -1291,7 +1291,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxide_kernel"
|
name = "oxide_kernel"
|
||||||
version = "0.8.37"
|
version = "0.8.38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
@@ -1306,7 +1306,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxide_liveness"
|
name = "oxide_liveness"
|
||||||
version = "0.8.37"
|
version = "0.8.38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"oxide_bytecode",
|
"oxide_bytecode",
|
||||||
"oxide_cfg",
|
"oxide_cfg",
|
||||||
@@ -1318,7 +1318,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxide_log"
|
name = "oxide_log"
|
||||||
version = "0.8.37"
|
version = "0.8.38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-appender",
|
"tracing-appender",
|
||||||
@@ -1327,7 +1327,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxide_parser"
|
name = "oxide_parser"
|
||||||
version = "0.8.37"
|
version = "0.8.38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"oxc_allocator",
|
"oxc_allocator",
|
||||||
"oxc_ast",
|
"oxc_ast",
|
||||||
@@ -1342,7 +1342,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxide_regalloc"
|
name = "oxide_regalloc"
|
||||||
version = "0.8.37"
|
version = "0.8.38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"oxide_bytecode",
|
"oxide_bytecode",
|
||||||
"oxide_cfg",
|
"oxide_cfg",
|
||||||
@@ -1358,7 +1358,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxide_runtime_api"
|
name = "oxide_runtime_api"
|
||||||
version = "0.8.37"
|
version = "0.8.38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-bigint",
|
"num-bigint",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
@@ -1370,7 +1370,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxide_test262"
|
name = "oxide_test262"
|
||||||
version = "0.8.37"
|
version = "0.8.38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"oxide_bytecode",
|
"oxide_bytecode",
|
||||||
"oxide_compiler",
|
"oxide_compiler",
|
||||||
@@ -1387,7 +1387,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxide_types"
|
name = "oxide_types"
|
||||||
version = "0.8.37"
|
version = "0.8.38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"hashbrown 0.14.5",
|
"hashbrown 0.14.5",
|
||||||
@@ -1399,7 +1399,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxide_vm"
|
name = "oxide_vm"
|
||||||
version = "0.8.37"
|
version = "0.8.38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.8.37"
|
version = "0.8.38"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
rust-version = "1.80"
|
rust-version = "1.80"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::process::ExitCode;
|
use std::process::{Command, ExitCode, Stdio};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ 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::protocol::{ServerRequest, ServerResponse};
|
use oxide_cli::server::protocol::{ServerRequest, ServerResponse};
|
||||||
use oxide_cli::server::server::{run_server, run_server_rm, RmServerConfig, ServerConfig};
|
use oxide_cli::server::server::{run_server, run_server_rm, RmServerConfig, ServerConfig};
|
||||||
|
use oxide_cli::server::sidecar;
|
||||||
use oxide_compiler::compiler::{compiled_module_hash, Compiler};
|
use oxide_compiler::compiler::{compiled_module_hash, Compiler};
|
||||||
use oxide_compiler::compiler_error;
|
use oxide_compiler::compiler_error;
|
||||||
use oxide_kernel::kernel::{KernelConfig, KernelCore};
|
use oxide_kernel::kernel::{KernelConfig, KernelCore};
|
||||||
@@ -134,7 +135,7 @@ enum ServerCommands {
|
|||||||
Version,
|
Version,
|
||||||
/// 清理残留文件(无 sidecar、陈旧 socket、杀刻度匹配进程)。
|
/// 清理残留文件(无 sidecar、陈旧 socket、杀刻度匹配进程)。
|
||||||
Cleanup,
|
Cleanup,
|
||||||
/// 向持久 server 发送 yield 请求(排空退出后重新接管 socket 路径)。
|
/// 向存活 server 发关闭请求,等其退出后拉起新 server 进程。
|
||||||
Restart,
|
Restart,
|
||||||
/// 查看 server 日志。
|
/// 查看 server 日志。
|
||||||
Log,
|
Log,
|
||||||
@@ -207,8 +208,8 @@ fn main() -> ExitCode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// server 子命令分派:start(两形态)与 cleanup 为最小实现,
|
/// server 子命令分派:start(两形态)与 cleanup 为最小实现,
|
||||||
/// version/status/health/info/stop 五臂经控制客户端为真实实现,
|
/// version/status/health/info/stop/restart 六臂为真实实现,
|
||||||
/// 其余三臂为 not_implemented 占位。
|
/// 其余两臂为 not_implemented 占位。
|
||||||
///
|
///
|
||||||
/// # 步骤
|
/// # 步骤
|
||||||
/// 1. Start:rm 为假调持久 server 入口(well-known 路径),rm 为真调独立
|
/// 1. Start:rm 为假调持久 server 入口(well-known 路径),rm 为真调独立
|
||||||
@@ -217,7 +218,9 @@ fn main() -> ExitCode {
|
|||||||
/// 已清理退 0,拒绝与杀进程失败退 1)并打印结果。
|
/// 已清理退 0,拒绝与杀进程失败退 1)并打印结果。
|
||||||
/// 3. 五个控制臂(version/status/health/info/stop):发对应控制请求,
|
/// 3. 五个控制臂(version/status/health/info/stop):发对应控制请求,
|
||||||
/// 成功打印渲染文本退 0,失败打印错误消息退 1。
|
/// 成功打印渲染文本退 0,失败打印错误消息退 1。
|
||||||
/// 4. 其余三臂:not_implemented 占位(退出码 2)。
|
/// 4. Restart:发关闭请求等旧 server 退出,拉起新 server 进程并探测就绪
|
||||||
|
/// (详见 restart_command)。
|
||||||
|
/// 5. 其余两臂:not_implemented 占位(退出码 2)。
|
||||||
///
|
///
|
||||||
/// # 边界与前提
|
/// # 边界与前提
|
||||||
/// - `--idle-timeout` 不带 --rm 时静默忽略(语义门控归后续任务)。
|
/// - `--idle-timeout` 不带 --rm 时静默忽略(语义门控归后续任务)。
|
||||||
@@ -266,7 +269,7 @@ fn server_command(command: ServerCommands) -> ExitCode {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
ServerCommands::Stop => control_arm(&ServerRequest::Shutdown, |response| match response {
|
ServerCommands::Stop => control_arm(&ServerRequest::Shutdown, |response| match response {
|
||||||
ServerResponse::Shutdown => Some("server 正在关闭".to_string()),
|
ServerResponse::Shutdown => Some("已停止".to_string()),
|
||||||
_ => None,
|
_ => None,
|
||||||
}),
|
}),
|
||||||
ServerCommands::Status => control_arm(&ServerRequest::Status, |response| match response {
|
ServerCommands::Status => control_arm(&ServerRequest::Status, |response| match response {
|
||||||
@@ -284,7 +287,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}\nsocket:{socket_path}\n进程号:{pid}"))
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}),
|
}),
|
||||||
@@ -292,7 +295,7 @@ fn server_command(command: ServerCommands) -> ExitCode {
|
|||||||
ServerResponse::Version { version } => Some(version.clone()),
|
ServerResponse::Version { version } => Some(version.clone()),
|
||||||
_ => None,
|
_ => None,
|
||||||
}),
|
}),
|
||||||
ServerCommands::Restart => not_implemented("server restart"),
|
ServerCommands::Restart => restart_command(),
|
||||||
ServerCommands::Log => not_implemented("server log"),
|
ServerCommands::Log => not_implemented("server log"),
|
||||||
ServerCommands::Forge => not_implemented("server forge"),
|
ServerCommands::Forge => not_implemented("server forge"),
|
||||||
}
|
}
|
||||||
@@ -319,6 +322,81 @@ fn control_arm(request: &ServerRequest, render: fn(&ServerResponse) -> Option<St
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// restart 子命令:向存活 server 发关闭请求,等其退出后拉起新 server 进程。
|
||||||
|
///
|
||||||
|
/// # 步骤
|
||||||
|
/// 1. 发关闭请求;三分支错误以红色打印退 1,无已注册 server 时消息即预设
|
||||||
|
/// 不满足提示。
|
||||||
|
/// 2. 轮询 well-known sidecar 文件消失(50 毫秒间隔、10 秒截止)判旧 server
|
||||||
|
/// 排水完成;超时提示人工检查退 1。
|
||||||
|
/// 3. 用当前可执行文件拉起脱离的 `oxide server start`(标准流全空、不等待);
|
||||||
|
/// spawn 失败退 1。
|
||||||
|
/// 4. 轮询健康请求直至得 healthy(100 毫秒间隔、10 秒截止)判新 server 就绪;
|
||||||
|
/// 超时退 1。
|
||||||
|
/// 5. 打印「已重启」退 0。
|
||||||
|
///
|
||||||
|
/// # 边界与前提
|
||||||
|
/// - 新 server 用缺省配置拉起(worker 数回落宿主核数),不继承旧 server 旗标。
|
||||||
|
///
|
||||||
|
/// # 副作用
|
||||||
|
/// - 旧 server 排空退出并删除 sidecar 与 socket 文件。
|
||||||
|
/// - 新 server 为脱离进程,CLI 退出后由 init 收领继续运行。
|
||||||
|
fn restart_command() -> ExitCode {
|
||||||
|
// 发关闭请求:三分支错误都以红色打印退 1,无 server 时消息即预设不满足提示。
|
||||||
|
if let Err(err) = send_control_request(&ServerRequest::Shutdown) {
|
||||||
|
eprintln!("{}", Red.paint(err.to_string()));
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等旧 server 退出:sidecar 是退出序列的最后删除项,消失即排水完成。
|
||||||
|
let sidecar_path = sidecar::well_known_sidecar_path();
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(10);
|
||||||
|
while sidecar_path.exists() {
|
||||||
|
if Instant::now() >= deadline {
|
||||||
|
eprintln!("{}", Red.paint("server 未在 10 秒内退出,可用 `oxide server cleanup` 人工检查"));
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_millis(50));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拉起新 server:脱离进程,标准流全空,不等待。
|
||||||
|
let exe = match std::env::current_exe() {
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("{}", Red.paint(format!("获取当前可执行文件路径失败:{err}")));
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(err) = Command::new(exe)
|
||||||
|
.args(["server", "start"])
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
{
|
||||||
|
eprintln!("{}", Red.paint(format!("拉起新 server 失败:{err}")));
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 就绪探测:健康请求通过即新 server 的监听与 accept 循环在运行。
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(10);
|
||||||
|
loop {
|
||||||
|
let ready =
|
||||||
|
matches!(send_control_request(&ServerRequest::Health), Ok(ServerResponse::Health { healthy: true }));
|
||||||
|
if ready {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if Instant::now() >= deadline {
|
||||||
|
eprintln!("{}", Red.paint("新 server 未在 10 秒内就绪"));
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("已重启");
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
/// 启动结果到退出码:成功退 0,错误以红色打印到 stderr 后退 1。
|
/// 启动结果到退出码:成功退 0,错误以红色打印到 stderr 后退 1。
|
||||||
fn start_result(result: Result<(), oxide_cli::server::server::ServerError>) -> ExitCode {
|
fn start_result(result: Result<(), oxide_cli::server::server::ServerError>) -> ExitCode {
|
||||||
match result {
|
match result {
|
||||||
|
|||||||
@@ -36,8 +36,10 @@ impl std::fmt::Display for ClientError {
|
|||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
ClientError::Connect(msg) => write!(f, "{msg}"),
|
ClientError::Connect(msg) => write!(f, "{msg}"),
|
||||||
ClientError::Protocol(err) => write!(f, "响应帧畸形:{err}"),
|
ClientError::Protocol(err) => {
|
||||||
ClientError::Server(msg) => write!(f, "server 以错误帧答复:{msg}"),
|
write!(f, "响应帧畸形:{err},server 版本可能不匹配,可用 `oxide server restart` 更新")
|
||||||
|
}
|
||||||
|
ClientError::Server(msg) => write!(f, "server 拒绝请求:{msg}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
//! server CLI 集成测试:真实二进制的分派面(help 列子命令、占位臂退出码、
|
//! server CLI 集成测试:真实二进制的分派面(help 列子命令、占位臂退出码、
|
||||||
//! cleanup 幂等、start 端到端)。
|
//! cleanup 幂等、start 与 restart 端到端)。
|
||||||
//!
|
//!
|
||||||
//! start 端到端与 cleanup 走 well-known 全局路径(每用户单例):测试先探活,
|
//! start / restart 端到端与 cleanup 走 well-known 全局路径(每用户单例):
|
||||||
//! socket 存活即 panic 不抢占存活 server;两枚触碰全局路径的测试经同一把
|
//! 测试先探活,socket 存活即 panic 不抢占存活 server;三枚触碰全局路径的
|
||||||
//! 锁串行,避免相互干扰。
|
//! 测试经同一把锁串行,避免相互干扰。
|
||||||
|
|
||||||
use std::io::{BufReader, Write};
|
use std::io::{BufReader, Write};
|
||||||
use std::os::unix::net::UnixStream;
|
use std::os::unix::net::UnixStream;
|
||||||
@@ -48,13 +48,13 @@ fn server_help_lists_subcommands() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 三个占位臂各退 2,stderr 含 not yet implemented。
|
/// 两个占位臂各退 2,stderr 含 not yet implemented。
|
||||||
///
|
///
|
||||||
/// version/status/health/info/stop 五臂已由控制客户端接管(无 server 时退 1),
|
/// version/status/health/info/stop/restart 六臂已是真实实现(无 server 时退 1),
|
||||||
/// 不在此列;restart/log/forge 仍是占位。
|
/// 不在此列;log/forge 仍是占位。
|
||||||
#[test]
|
#[test]
|
||||||
fn server_stub_subcommands_exit_2() {
|
fn server_stub_subcommands_exit_2() {
|
||||||
for sub in ["restart", "log", "forge"] {
|
for sub in ["log", "forge"] {
|
||||||
let output = oxide(&["server", sub]);
|
let output = oxide(&["server", sub]);
|
||||||
assert_eq!(output.status.code(), Some(2), "{sub} 应退 2");
|
assert_eq!(output.status.code(), Some(2), "{sub} 应退 2");
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
@@ -157,3 +157,82 @@ fn server_start_e2e() {
|
|||||||
assert!(!socket.exists(), "socket 文件应被删除");
|
assert!(!socket.exists(), "socket 文件应被删除");
|
||||||
assert!(!sidecar_path.exists(), "sidecar 文件应被删除");
|
assert!(!sidecar_path.exists(), "sidecar 文件应被删除");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// restart 端到端:起真实子进程 A,跑 `oxide server restart`(发关闭、等退出、
|
||||||
|
/// 拉起新 server、就绪探测),断言退 0 且打印「已重启」,旧进程退 0,新 sidecar
|
||||||
|
/// 进程号不同且版本为构建期版本,新 server 健康与版本请求正常,收尾关闭后文件被删。
|
||||||
|
#[test]
|
||||||
|
fn server_restart_e2e() {
|
||||||
|
let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒");
|
||||||
|
let socket = sidecar::well_known_socket_path();
|
||||||
|
let sidecar_path = sidecar::well_known_sidecar_path();
|
||||||
|
|
||||||
|
// 探活先行:存活 server 占用全局路径时 panic,不抢占。
|
||||||
|
assert!(!sidecar::is_server_alive(&socket), "存活 server 占用 well-known 路径,测试不抢占");
|
||||||
|
|
||||||
|
// 起子进程 A:worker 数压到 2,轮询 socket 文件出现(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 文件未出现");
|
||||||
|
std::thread::sleep(Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读 sidecar 取旧进程号。
|
||||||
|
let old_id = sidecar::read_identity(&sidecar_path).expect("读旧 sidecar 应成功");
|
||||||
|
|
||||||
|
// 跑 restart:断言退 0、stdout 含「已重启」。
|
||||||
|
let output = oxide(&["server", "restart"]);
|
||||||
|
assert_eq!(output.status.code(), Some(0), "restart 应退 0");
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
assert!(stdout.contains("已重启"), "restart 应打印「已重启」:{stdout}");
|
||||||
|
|
||||||
|
// 断言子进程 A 以退出码 0 退出(30 秒截止,try_wait 轮询)。
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(30);
|
||||||
|
loop {
|
||||||
|
match child.try_wait() {
|
||||||
|
Ok(Some(status)) => {
|
||||||
|
assert_eq!(status.code(), Some(0), "旧 server 应以退出码 0 退出:{status:?}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
assert!(Instant::now() < deadline, "旧 server 应在有界时间内退出");
|
||||||
|
std::thread::sleep(Duration::from_millis(50));
|
||||||
|
}
|
||||||
|
Err(err) => panic!("等待子进程失败:{err}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 断言新 sidecar 存在、进程号与旧进程号不同、版本等于构建期版本。
|
||||||
|
let new_id = sidecar::read_identity(&sidecar_path).expect("读新 sidecar 应成功");
|
||||||
|
assert_ne!(new_id.pid, old_id.pid, "新 server 进程号应不同于旧 server");
|
||||||
|
assert_eq!(new_id.version, env!("CARGO_PKG_VERSION"), "新 sidecar 版本应与构建期版本一致");
|
||||||
|
|
||||||
|
// 连接新 server:健康请求得 healthy,版本请求得构建期版本。
|
||||||
|
let mut stream = UnixStream::connect(&socket).expect("连接新 server 应成功");
|
||||||
|
match send_and_recv(&mut stream, &ServerRequest::Health) {
|
||||||
|
ServerResponse::Health { healthy } => assert!(healthy, "新 server 应健康"),
|
||||||
|
other => panic!("应得 Health 帧,实得 {other:?}"),
|
||||||
|
}
|
||||||
|
match send_and_recv(&mut stream, &ServerRequest::Version) {
|
||||||
|
ServerResponse::Version { version } => {
|
||||||
|
assert_eq!(version, env!("CARGO_PKG_VERSION"), "版本应与构建期版本一致");
|
||||||
|
}
|
||||||
|
other => panic!("应得 Version 帧,实得 {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收尾:发关闭请求,等新 server 退出,断言 socket 与 sidecar 文件被删。
|
||||||
|
let response = send_and_recv(&mut stream, &ServerRequest::Shutdown);
|
||||||
|
assert!(matches!(response, ServerResponse::Shutdown), "应得关闭确认帧:{response:?}");
|
||||||
|
drop(stream);
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(30);
|
||||||
|
while sidecar_path.exists() {
|
||||||
|
assert!(Instant::now() < deadline, "新 server 应在有界时间内退出");
|
||||||
|
std::thread::sleep(Duration::from_millis(50));
|
||||||
|
}
|
||||||
|
assert!(!socket.exists(), "socket 文件应被删除");
|
||||||
|
assert!(!sidecar_path.exists(), "sidecar 文件应被删除");
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user