diff --git a/Cargo.lock b/Cargo.lock index 01c44ed7..903a150d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,7 +1160,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.14.11" +version = "0.14.12" dependencies = [ "chrono", "fast-float", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.14.11" +version = "0.14.12" dependencies = [ "num-bigint", "oxide_log", @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.14.11" +version = "0.14.12" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1204,12 +1204,13 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.14.11" +version = "0.14.12" dependencies = [ "ansi_term", "bumpalo", "clap", "ctrlc", + "libc", "oxide_bytecode", "oxide_code_cache", "oxide_compiler", @@ -1226,7 +1227,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.14.11" +version = "0.14.12" dependencies = [ "lru", "oxide_bytecode", @@ -1235,7 +1236,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.14.11" +version = "0.14.12" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1255,7 +1256,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.14.11" +version = "0.14.12" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1269,7 +1270,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.14.11" +version = "0.14.12" dependencies = [ "num-bigint", "oxc_allocator", @@ -1284,7 +1285,7 @@ dependencies = [ [[package]] name = "oxide_engine" -version = "0.14.11" +version = "0.14.12" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1296,7 +1297,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.14.11" +version = "0.14.12" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1305,7 +1306,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.14.11" +version = "0.14.12" dependencies = [ "bumpalo", "dashmap", @@ -1319,7 +1320,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.14.11" +version = "0.14.12" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1331,7 +1332,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.14.11" +version = "0.14.12" dependencies = [ "tracing", "tracing-appender", @@ -1340,7 +1341,7 @@ dependencies = [ [[package]] name = "oxide_parser" -version = "0.14.11" +version = "0.14.12" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1355,7 +1356,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.14.11" +version = "0.14.12" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1371,7 +1372,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.14.11" +version = "0.14.12" dependencies = [ "num-bigint", "num-traits", @@ -1384,7 +1385,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.14.11" +version = "0.14.12" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1402,7 +1403,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.14.11" +version = "0.14.12" dependencies = [ "bumpalo", "dashmap", @@ -1415,7 +1416,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.14.11" +version = "0.14.12" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 05ad2374..f023b1de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ members = [ ] [workspace.package] -version = "0.14.11" +version = "0.14.12" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_cli/Cargo.toml b/crates/oxide_cli/Cargo.toml index b387151b..aa046d6e 100644 --- a/crates/oxide_cli/Cargo.toml +++ b/crates/oxide_cli/Cargo.toml @@ -22,6 +22,7 @@ oxide_engine = { path = "../oxide_engine" } oxide_log = { path = "../oxide_log" } bumpalo.workspace = true clap.workspace = true +libc = "0.2" ctrlc.workspace = true rustyline.workspace = true ansi_term.workspace = true diff --git a/crates/oxide_cli/src/server/server.rs b/crates/oxide_cli/src/server/server.rs index d9aaa42a..f863ba15 100644 --- a/crates/oxide_cli/src/server/server.rs +++ b/crates/oxide_cli/src/server/server.rs @@ -1317,6 +1317,9 @@ mod tests { /// 正常退出,socket 文件被删除,全局 well-known 路径前后不变。 #[test] fn rm_eval_then_eof_exits() { + // 跨进程文件锁:与写全局路径的集成测试二进制串行,保证前后不变断言 + // 不被并发写污染。 + let _file_lock = sidecar::WellKnownLock::acquire().expect("全局路径文件锁应可取"); let config = rm_config("eval_eof"); let well_known_sidecar = sidecar::well_known_sidecar_path(); let well_known_socket = sidecar::well_known_socket_path(); diff --git a/crates/oxide_cli/src/server/sidecar.rs b/crates/oxide_cli/src/server/sidecar.rs index c1eb2cff..eb9992b6 100644 --- a/crates/oxide_cli/src/server/sidecar.rs +++ b/crates/oxide_cli/src/server/sidecar.rs @@ -11,8 +11,9 @@ //! - 日志文件由 sidecar 路径按扩展名派生(`.log`),与 sidecar、socket 同主名 //! 同目录;有意不随退出删除(跨重启的诊断工件),不在清理命令范围内。 -use std::fs::{self, OpenOptions}; +use std::fs::{self, File, OpenOptions}; use std::io::{self, Write}; +use std::os::unix::io::AsRawFd; use std::os::unix::net::UnixStream; use std::path::{Path, PathBuf}; use std::process::Command; @@ -93,6 +94,59 @@ pub fn well_known_log_path() -> PathBuf { well_known_sidecar_path().with_extension("log") } +/// well-known 全局路径排他锁文件:`$TMPDIR/oxide-.lock`(与 sidecar 同主名)。 +pub fn well_known_lock_path() -> PathBuf { + well_known_sidecar_path().with_extension("lock") +} + +/// well-known 全局路径排他锁:fcntl 文件锁,跨进程互斥。 +/// +/// 持锁期间保护 well-known 全局路径(sidecar、socket、日志)的读写; +/// 锁随文件描述符关闭(Drop)自动释放,进程消亡时内核一并释放。 +/// +/// # 边界与前提 +/// - fcntl 锁以进程为单位:同进程多线程并发取锁不互斥,进程内串行化 +/// 由调用方自己的进程内互斥负责。 +/// +/// # 副作用 +/// - 创建锁文件(不存在时)。 +/// +/// # 注意事项 +/// - 锁文件不随释放删除:删除会破坏他进程已打开描述符上的锁,互斥失效。 +pub struct WellKnownLock { + /// 持有文件描述符:锁随描述符关闭(Drop)释放,字段存在即保活。 + #[allow(dead_code)] + file: File, +} + +impl WellKnownLock { + /// 取 well-known 全局路径排他锁(阻塞直至持有)。 + /// + /// # 步骤 + /// 1. 打开锁文件(不存在则创建)。 + /// 2. fcntl 阻塞取排他锁(F_SETLKW)。 + pub fn acquire() -> io::Result { + let file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(well_known_lock_path())?; + // SAFETY: 文件描述符在 file 生命周期内有效,锁结构体按值传给内核只读。 + let lock = libc::flock { + l_type: libc::F_WRLCK as _, + l_whence: libc::SEEK_SET as _, + l_start: 0, + l_len: 0, + l_pid: 0, + }; + let rc = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_SETLKW, &lock) }; + if rc < 0 { + return Err(io::Error::last_os_error()); + } + Ok(WellKnownLock { file }) + } +} + /// O_EXCL 原子创建 sidecar(文件已存在时创建失败)。 /// /// # 步骤 @@ -477,4 +531,23 @@ mod tests { assert_eq!(log.file_stem(), sidecar.file_stem(), "日志与 sidecar 应同主名"); assert_eq!(log.parent(), sidecar.parent(), "日志与 sidecar 应同目录"); } + + /// 锁路径:`.lock` 扩展名,与 sidecar 路径同主名同目录。 + #[test] + fn well_known_lock_path_derives_from_sidecar() { + let lock = well_known_lock_path(); + let sidecar = well_known_sidecar_path(); + assert_eq!(lock.extension().and_then(|e| e.to_str()), Some("lock"), "锁路径应以 .lock 结尾"); + assert_eq!(lock.file_stem(), sidecar.file_stem(), "锁与 sidecar 应同主名"); + assert_eq!(lock.parent(), sidecar.parent(), "锁与 sidecar 应同目录"); + } + + /// 全局路径锁:取锁与释放后重取均成功(fcntl 锁以进程为单位, + /// 同进程重取不自我阻塞)。 + #[test] + fn well_known_lock_acquire_release_reacquire() { + let lock = WellKnownLock::acquire().expect("取锁应成功"); + drop(lock); + let _again = WellKnownLock::acquire().expect("释放后重取应成功"); + } } diff --git a/crates/oxide_cli/tests/server_cli.rs b/crates/oxide_cli/tests/server_cli.rs index 2af84179..1dfdb60b 100644 --- a/crates/oxide_cli/tests/server_cli.rs +++ b/crates/oxide_cli/tests/server_cli.rs @@ -4,21 +4,40 @@ //! //! start / restart / watchdog 端到端与 cleanup 走 well-known 全局路径 //! (每用户单例):测试先探活,socket 存活即 panic 不抢占存活 server; -//! 触碰全局路径的测试经同一把锁串行,避免相互干扰。 +//! 触碰全局路径的测试经双重锁(进程内互斥加跨进程文件锁)串行,避免相互 +//! 干扰(含与单测二进制的并行干扰)。 use std::io::{BufRead, BufReader, Write}; use std::os::unix::net::UnixStream; use std::process::{Command, Output}; -use std::sync::Mutex; +use std::sync::{Mutex, MutexGuard}; 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; -/// well-known 全局路径互斥锁:同测试二进制内触碰全局路径的测试串行执行。 +/// well-known 全局路径进程内互斥锁:同测试二进制内触碰全局路径的测试串行 +/// 执行(fcntl 锁以进程为单位,不覆盖同进程多线程)。 static WELL_KNOWN_LOCK: Mutex<()> = Mutex::new(()); +/// well-known 全局路径双重锁守卫:进程内互斥(同二进制线程串行)加跨进程 +/// fcntl 文件锁(nextest 并行独立进程串行)。 +struct WellKnownGuard { + _in_process: MutexGuard<'static, ()>, + _cross_process: sidecar::WellKnownLock, +} + +/// 取 well-known 全局路径双重锁,守卫随 drop 释放。 +fn lock_well_known() -> WellKnownGuard { + let in_process = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒"); + let cross_process = sidecar::WellKnownLock::acquire().expect("全局路径文件锁应可取"); + WellKnownGuard { + _in_process: in_process, + _cross_process: cross_process, + } +} + /// 运行 oxide 二进制并返回输出。 fn oxide(args: &[&str]) -> Output { Command::new(env!("CARGO_BIN_EXE_oxide")) @@ -93,7 +112,7 @@ fn server_help_lists_subcommands() { /// well-known 日志文件(无存活 server 时删除安全)。 #[test] fn server_log_no_file_exit_1() { - let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒"); + let _guard = lock_well_known(); assert!( !sidecar::is_server_alive(&sidecar::well_known_socket_path()), "存活 server 占用 well-known 路径,测试不抢占" @@ -113,7 +132,7 @@ fn server_log_no_file_exit_1() { /// 走 well-known 全局路径:先探活,存活 server 占用时 panic 不抢占。 #[test] fn server_control_arms_no_server_exit_1() { - let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒"); + let _guard = lock_well_known(); assert!( !sidecar::is_server_alive(&sidecar::well_known_socket_path()), "存活 server 占用 well-known 路径,测试不抢占" @@ -132,7 +151,7 @@ fn server_control_arms_no_server_exit_1() { /// 走 well-known 全局路径:先探活,存活 server 占用时 panic 不抢占。 #[test] fn server_forge_no_server_exit_1() { - let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒"); + let _guard = lock_well_known(); assert!( !sidecar::is_server_alive(&sidecar::well_known_socket_path()), "存活 server 占用 well-known 路径,测试不抢占" @@ -147,7 +166,7 @@ fn server_forge_no_server_exit_1() { /// cleanup 退 0(幂等:有残留则清理、无残留则报无残留),事后全局路径无文件。 #[test] fn server_cleanup_exit_0() { - let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒"); + let _guard = lock_well_known(); assert!( !sidecar::is_server_alive(&sidecar::well_known_socket_path()), "存活 server 占用 well-known 路径,测试不抢占" @@ -164,7 +183,7 @@ fn server_cleanup_exit_0() { /// 进程退出,socket 与 sidecar 文件被删。 #[test] fn server_start_e2e() { - let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒"); + let _guard = lock_well_known(); let socket = sidecar::well_known_socket_path(); let sidecar_path = sidecar::well_known_sidecar_path(); @@ -213,7 +232,7 @@ fn server_start_e2e() { /// server 即通过)。 #[test] fn server_start_when_running() { - let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒"); + let _guard = lock_well_known(); let socket = sidecar::well_known_socket_path(); let sidecar_path = sidecar::well_known_sidecar_path(); @@ -251,7 +270,7 @@ fn server_start_when_running() { /// 进程号不同且版本为构建期版本,新 server 健康与版本请求正常,收尾关闭后文件被删。 #[test] fn server_restart_e2e() { - let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒"); + let _guard = lock_well_known(); let socket = sidecar::well_known_socket_path(); let sidecar_path = sidecar::well_known_sidecar_path(); @@ -331,7 +350,7 @@ fn server_restart_e2e() { /// (Info 级无 ERROR 行),关闭后日志文件有意保留。 #[test] fn server_log_e2e() { - let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒"); + 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(); @@ -412,7 +431,7 @@ fn server_log_e2e() { /// 打印 gc 与清缓存行;关闭后 socket 与 sidecar 文件被删。 #[test] fn server_forge_e2e() { - let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒"); + let _guard = lock_well_known(); let socket = sidecar::well_known_socket_path(); let sidecar_path = sidecar::well_known_sidecar_path(); @@ -471,7 +490,7 @@ fn server_forge_e2e() { /// 关闭后 watchdog 退 0 且 stdout 含崩溃提示与日志尾部。 #[test] fn server_watchdog_e2e() { - let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒"); + let _guard = lock_well_known(); let socket = sidecar::well_known_socket_path(); let sidecar_path = sidecar::well_known_sidecar_path(); @@ -574,7 +593,7 @@ fn server_watchdog_e2e() { /// sidecar 消失、server 进程已退出(停 watchdog 即优雅停 server)。 #[test] fn server_watchdog_sigint_stops_server() { - let _guard = WELL_KNOWN_LOCK.lock().expect("全局路径锁不应中毒"); + let _guard = lock_well_known(); let socket = sidecar::well_known_socket_path(); let sidecar_path = sidecar::well_known_sidecar_path();