diff --git a/Cargo.lock b/Cargo.lock index 1539229..eeff1b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,6 +105,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" @@ -210,6 +216,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -432,6 +447,26 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inotify" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +dependencies = [ + "bitflags 2.10.0", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "itoa" version = "1.0.16" @@ -448,6 +483,26 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -512,6 +567,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] @@ -524,6 +580,8 @@ dependencies = [ "axum-extra", "chrono", "futures-util", + "notify", + "notify-debouncer-mini", "serde", "serde_json", "serde_yaml", @@ -553,6 +611,42 @@ dependencies = [ "version_check", ] +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.10.0", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-debouncer-mini" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a689eb4262184d9a1727f9087cd03883ea716682ab03ed24efec57d7716dccb8" +dependencies = [ + "log", + "notify", + "notify-types", + "tempfile", +] + +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -664,7 +758,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.10.0", ] [[package]] @@ -673,7 +767,7 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", @@ -1159,7 +1253,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags", + "bitflags 2.10.0", "hashbrown 0.15.5", "indexmap", "semver", @@ -1380,7 +1474,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.10.0", "indexmap", "log", "serde", diff --git a/Cargo.toml b/Cargo.toml index 9fdccff..0c93bd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,8 @@ tracing-subscriber = "0.3.22" # 性能优化:快速哈希(可选,用于路由匹配) #dashmap = "7.0.0-rc2" # 热加载支持(扩展功能) -#notify = "8.2.0" +notify = "8.2.0" +notify-debouncer-mini = "0.6.0" # 路径处理 #pathdiff = "0.2.3" diff --git a/mocks/v1/auth/login.yaml b/mocks/v1/auth/login.yaml index 3ab6dad..76a8533 100644 --- a/mocks/v1/auth/login.yaml +++ b/mocks/v1/auth/login.yaml @@ -25,4 +25,4 @@ response: "msg": "success" } settings: - delay_ms: 200 # 模拟真实网络延迟 \ No newline at end of file + delay_ms: 2000 # 模拟真实网络延迟 diff --git a/src/handler.rs b/src/handler.rs index f6e0d4f..718f9d9 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -5,14 +5,14 @@ use axum::{ response::{IntoResponse, Response}, }; use std::collections::HashMap; -use std::sync::Arc; -use tokio_util::io::ReaderStream; // 需在 Cargo.toml 确认有 tokio-util +use std::sync::{Arc, RwLock}; +use tokio_util::io::ReaderStream; use crate::router::MockRouter; -/// 共享的应用状态 +/// 共享的应用状态,router 由 RwLock 保护以支持热重载 pub struct AppState { - pub router: MockRouter, + pub router: RwLock, } /// 全局统一请求处理函数 @@ -26,7 +26,7 @@ pub async fn mock_handler( let path = req.uri().path().to_string(); let method_str = method.as_str().to_string(); - // 将 Axum HeaderMap 转换为简单的 HashMap 供 Router 使用 + // 1. 将需要的数据克隆出来,断开与 req 的借用关系 let body_bytes = match axum::body::to_bytes(req.into_body(), 10 * 1024 * 1024).await { Ok(bytes) => bytes, Err(_) => { @@ -39,40 +39,41 @@ pub async fn mock_handler( let incoming_json: Option = serde_json::from_slice(&body_bytes).ok(); + // 2. 将 Axum HeaderMap 转换为简单的 HashMap let mut req_headers = HashMap::new(); for (name, value) in headers.iter() { if let Ok(v) = value.to_str() { req_headers.insert(name.as_str().to_string(), v.to_string()); } } - // 2. 执行匹配逻辑 - if let Some(rule) = - state - .router - .match_rule(&method_str, &path, ¶ms, &req_headers, &incoming_json) - { - // 3. 处理模拟延迟 + + // 3. 执行匹配逻辑:先获取读锁 (Read Lock) + let maybe_rule = { + let router = state.router.read().expect("Failed to acquire read lock"); + router.match_rule(&method_str, &path, ¶ms, &req_headers, &incoming_json).cloned() + // 使用 .cloned() 以便尽早释放读锁,避免阻塞热重载写锁 + }; + + if let Some(rule) = maybe_rule { + // 4. 处理模拟延迟 if let Some(ref settings) = rule.settings { if let Some(delay) = settings.delay_ms { tokio::time::sleep(std::time::Duration::from_millis(delay)).await; } } - // 4. 构建响应基础信息 + // 5. 构建响应 let status = StatusCode::from_u16(rule.response.status).unwrap_or(StatusCode::OK); let mut response_builder = Response::builder().status(status); - // 注入 YAML 定义的 Header if let Some(ref h) = rule.response.headers { for (k, v) in h { - // println!("{}:{}",k.clone(), v.clone()); response_builder = response_builder.header(k, v); } } - // 5. 执行 Smart Body 协议逻辑 + // 6. Smart Body 逻辑 if let Some(file_path) = rule.response.get_file_path() { - // A. 文件模式:异步打开文件并转换为流,实现低内存占用 match tokio::fs::File::open(file_path).await { Ok(file) => { let stream = ReaderStream::new(file); @@ -88,13 +89,13 @@ pub async fn mock_handler( .unwrap(), } } else { - // B. 内联模式:直接返回字符串内容 + // 内联模式:直接返回字符串内容 response_builder .body(Body::from(rule.response.body.clone())) .unwrap() } } else { - println!("请求头{:?}", req_headers.clone()); + println!("请求头{:?}", req_headers); // 匹配失败返回 404 Response::builder() .status(StatusCode::NOT_FOUND) diff --git a/src/main.rs b/src/main.rs index 8436f8c..7609202 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,10 @@ use std::net::SocketAddr; use std::path::Path; -use std::sync::Arc; +use std::sync::{Arc, RwLock}; +use std::time::Duration; use axum::{routing::{any, post}, Router}; +use notify_debouncer_mini::{new_debouncer, notify::*}; + use mock_server::loader::MockLoader; use mock_server::router::MockRouter; use mock_server::handler::{mock_handler, AppState}; @@ -9,34 +12,58 @@ use mock_server::upload::upload_handler; #[tokio::main] async fn main() { - // 1. 初始化日志(建议添加,方便观察加载情况) - // 需要在 Cargo.toml 添加 tracing-subscriber = "0.3" tracing_subscriber::fmt::init(); - // 2. 递归加载所有的 YAML 配置文件 + // 1. 确保 mocks 目录存在 let mocks_dir = Path::new("./mocks"); if !mocks_dir.exists() { println!("Warning: 'mocks/' directory not found. Creating it..."); std::fs::create_dir_all(mocks_dir).unwrap(); } - println!("Scanning mocks directory..."); - let index = MockLoader::load_all_from_dir(mocks_dir); - - // 3. 确保 storage 目录存在 + // 2. 确保 storage 目录存在 let storage_dir = Path::new("./storage"); if !storage_dir.exists() { println!("Creating storage directory..."); std::fs::create_dir_all(storage_dir).unwrap(); } - // 4. 构建路由引擎并包装为共享状态 - let router_engine = MockRouter::new(index); + // 3. 初始加载 Mock 配置 + println!("Scanning mocks directory..."); + let index = MockLoader::load_all_from_dir(mocks_dir); + + // 4. 构建共享状态(使用 RwLock 支持热重载) let shared_state = Arc::new(AppState { - router: router_engine, + router: RwLock::new(MockRouter::new(index)), }); - // 5. 配置 Axum 路由 + // 5. 设置热加载监听器 + let state_for_watcher = shared_state.clone(); + let watch_path = mocks_dir.to_path_buf(); + let (tx, rx) = std::sync::mpsc::channel(); + + // 200ms 防抖,防止编辑器保存文件时产生多次干扰 + let mut debouncer = new_debouncer(Duration::from_millis(200), tx).unwrap(); + debouncer.watcher().watch(&watch_path, RecursiveMode::Recursive).unwrap(); + + // 启动异步任务监听文件变动 + tokio::spawn(async move { + while let Ok(res) = rx.recv() { + match res { + Ok(_) => { + println!("🔄 Detecting changes in mocks/, reloading..."); + let new_index = MockLoader::load_all_from_dir(&watch_path); + // 获取写锁 (Write Lock) 更新索引 + let mut writer = state_for_watcher.router.write().expect("Failed to acquire write lock"); + *writer = MockRouter::new(new_index); + println!("✅ Mocks reloaded successfully."); + } + Err(e) => eprintln!("Watcher error: {:?}", e), + } + } + }); + + // 6. 配置 Axum 路由 // 文件上传路由:POST /api/upload // 其他所有请求由 mock_handler 处理 let app = Router::new() @@ -44,12 +71,13 @@ async fn main() { .fallback(any(mock_handler)) .with_state(shared_state); - // 6. 启动服务 + // 7. 启动服务 let addr = SocketAddr::from(([127, 0, 0, 1], 8080)); println!("🚀 Rust Mock Server is running on http://{}", addr); println!("📁 File upload endpoint: POST http://{}/api/upload", addr); + println!("🔄 Hot reload enabled for mocks/ directory"); println!("Ready to handle requests based on your YAML definitions."); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); axum::serve(listener, app).await.unwrap(); -} \ No newline at end of file +}