mcp_common/
process_compat.rs1#[cfg(windows)]
36use tracing::{info, warn};
37
38#[cfg(windows)]
56pub fn check_windows_command(command: &str) {
57 use std::path::Path;
58
59 let cmd_ext = Path::new(command)
60 .extension()
61 .and_then(|e| e.to_str())
62 .map(|s| s.to_ascii_lowercase());
63
64 match cmd_ext.as_deref() {
65 Some("cmd" | "bat") => {
66 warn!(
67 "[MCP] Windows 检测到 .cmd/.bat 命令: {} - 可能会弹 CMD 窗口!",
68 command
69 );
70 warn!("[MCP] 建议改用 node.exe 直接运行 JS 文件,或在配置中使用完整路径");
71 }
72 None => {
73 if command.contains("npx") {
75 warn!(
76 "[MCP] Windows 检测到 npx 命令: {} - 可能会弹 CMD 窗口!",
77 command
78 );
79 warn!("[MCP] 建议改用 node.exe 直接运行 JS 文件");
80 }
81 }
82 _ => {
83 info!("[MCP] Windows 检测到命令格式: {}", command);
84 }
85 }
86}
87
88#[cfg(not(windows))]
90pub fn check_windows_command(_command: &str) {
91 }
93
94#[cfg(target_os = "windows")]
117pub fn resolve_windows_command(command: &str) -> String {
118 use std::path::Path;
119
120 if Path::new(command).extension().is_some() {
122 return command.to_string();
123 }
124
125 if Path::new(command).is_absolute() {
127 return command.to_string();
128 }
129
130 let path_env = match std::env::var("PATH") {
132 Ok(p) => p,
133 Err(_) => return command.to_string(),
134 };
135
136 let extensions = [".cmd", ".exe", ".bat", ".ps1"];
138
139 for dir in path_env.split(';') {
141 let dir = dir.trim();
142 if dir.is_empty() {
143 continue;
144 }
145
146 for ext in &extensions {
148 let full_path = Path::new(dir).join(format!("{}{}", command, ext));
149 if full_path.exists() {
150 tracing::debug!(
151 "[MCP] Windows 命令解析: {} -> {}",
152 command,
153 full_path.display()
154 );
155 return format!("{}{}", command, ext);
157 }
158 }
159 }
160
161 command.to_string()
163}
164
165#[cfg(not(target_os = "windows"))]
167pub fn resolve_windows_command(command: &str) -> String {
168 command.to_string()
169}
170
171pub fn ensure_runtime_path(path: &str) -> String {
182 if let Ok(runtime_path) = std::env::var("NUWAX_APP_RUNTIME_PATH") {
183 let runtime_path = runtime_path.trim();
184 if !runtime_path.is_empty() {
185 let sep = if cfg!(windows) { ";" } else { ":" };
186
187 let runtime_segments: Vec<&str> =
189 runtime_path.split(sep).filter(|s| !s.is_empty()).collect();
190
191 let existing_segments: Vec<&str> = path
193 .split(sep)
194 .filter(|s| !s.is_empty() && !runtime_segments.contains(s))
195 .collect();
196
197 let merged: Vec<&str> = runtime_segments
198 .iter()
199 .copied()
200 .chain(existing_segments)
201 .collect();
202
203 let result = merged.join(sep);
204 if result != path {
205 tracing::info!(
206 "[ProcessCompat] 前置应用内置运行时到 PATH: {}",
207 runtime_path
208 );
209 }
210 return result;
211 }
212 }
213 path.to_string()
214}
215
216#[cfg(unix)]
239#[macro_export]
240macro_rules! wrap_process_v8 {
241 ($cmd:expr) => {{
242 use process_wrap::tokio::ProcessGroup;
243 $cmd.wrap(ProcessGroup::leader());
244 }};
245}
246
247#[cfg(windows)]
248#[macro_export]
249macro_rules! wrap_process_v8 {
250 ($cmd:expr) => {{
251 use process_wrap::tokio::{CreationFlags, JobObject};
252 use windows::Win32::System::Threading::{CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW};
253 $cmd.wrap(CreationFlags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP));
254 $cmd.wrap(JobObject);
255 }};
256}
257
258#[cfg(unix)]
281#[macro_export]
282macro_rules! wrap_process_v9 {
283 ($cmd:expr) => {{
284 use process_wrap::tokio::ProcessGroup;
285 $cmd.wrap(ProcessGroup::leader());
286 }};
287}
288
289#[cfg(windows)]
290#[macro_export]
291macro_rules! wrap_process_v9 {
292 ($cmd:expr) => {{
293 use process_wrap::tokio::{CreationFlags, JobObject};
294 use windows::Win32::System::Threading::{CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW};
295 $cmd.wrap(CreationFlags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP));
296 $cmd.wrap(JobObject);
297 }};
298}
299
300pub fn spawn_stderr_reader<T>(stderr: T, service_name: String) -> tokio::task::JoinHandle<()>
328where
329 T: tokio::io::AsyncRead + Unpin + Send + 'static,
330{
331 tokio::spawn(async move {
332 use tokio::io::{AsyncBufReadExt, BufReader};
333
334 let mut reader = BufReader::new(stderr);
335 let mut line = String::new();
336 loop {
337 line.clear();
338 match reader.read_line(&mut line).await {
339 Ok(0) => {
340 tracing::debug!("[子进程 stderr][{}] 读取结束 (EOF)", service_name);
342 break;
343 }
344 Ok(_) => {
345 let trimmed = line.trim();
346 if !trimmed.is_empty() {
347 tracing::warn!("[子进程 stderr][{}] {}", service_name, trimmed);
348 }
349 }
350 Err(e) => {
351 tracing::debug!("[子进程 stderr][{}] 读取错误: {}", service_name, e);
352 break;
353 }
354 }
355 }
356 })
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362
363 #[test]
364 fn test_check_windows_command_non_windows() {
365 check_windows_command("npx some-server");
367 check_windows_command("test.cmd");
368 }
369
370 #[test]
371 fn test_ensure_runtime_path_no_env() {
372 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
374 let result = ensure_runtime_path("/usr/bin:/usr/local/bin");
375 assert_eq!(result, "/usr/bin:/usr/local/bin");
376 }
377
378 #[test]
379 fn test_ensure_runtime_path_prepend() {
380 unsafe {
381 std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
382 }
383 let result = ensure_runtime_path("/usr/bin:/usr/local/bin");
384 assert_eq!(result, "/app/node/bin:/app/uv/bin:/usr/bin:/usr/local/bin");
385 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
386 }
387
388 #[test]
389 fn test_ensure_runtime_path_dedup() {
390 unsafe {
392 std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
393 }
394 let result = ensure_runtime_path("/app/node/bin:/opt/homebrew/bin:/usr/bin");
395 assert_eq!(
396 result,
397 "/app/node/bin:/app/uv/bin:/opt/homebrew/bin:/usr/bin"
398 );
399 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
400 }
401
402 #[test]
403 fn test_ensure_runtime_path_all_present() {
404 unsafe {
406 std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
407 }
408 let result = ensure_runtime_path("/app/uv/bin:/usr/bin:/app/node/bin");
409 assert_eq!(result, "/app/node/bin:/app/uv/bin:/usr/bin");
410 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
411 }
412
413 #[test]
414 fn test_ensure_runtime_path_double_node() {
415 unsafe {
417 std::env::set_var(
418 "NUWAX_APP_RUNTIME_PATH",
419 "/app/node/bin:/app/uv/bin:/app/debug",
420 );
421 }
422 let result = ensure_runtime_path(
423 "/app/node/bin:/app/node/bin:/app/uv/bin:/app/debug:/opt/homebrew/bin",
424 );
425 assert_eq!(
426 result,
427 "/app/node/bin:/app/uv/bin:/app/debug:/opt/homebrew/bin"
428 );
429 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
430 }
431}