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
94pub fn ensure_runtime_path(path: &str) -> String {
105 if let Ok(runtime_path) = std::env::var("NUWAX_APP_RUNTIME_PATH") {
106 let runtime_path = runtime_path.trim();
107 if !runtime_path.is_empty() {
108 let sep = if cfg!(windows) { ";" } else { ":" };
109
110 let runtime_segments: Vec<&str> =
112 runtime_path.split(sep).filter(|s| !s.is_empty()).collect();
113
114 let existing_segments: Vec<&str> = path
116 .split(sep)
117 .filter(|s| !s.is_empty() && !runtime_segments.contains(s))
118 .collect();
119
120 let merged: Vec<&str> = runtime_segments
121 .iter()
122 .copied()
123 .chain(existing_segments)
124 .collect();
125
126 let result = merged.join(sep);
127 if result != path {
128 tracing::info!(
129 "[ProcessCompat] 前置应用内置运行时到 PATH: {}",
130 runtime_path
131 );
132 }
133 return result;
134 }
135 }
136 path.to_string()
137}
138
139pub fn prepare_stdio_env(
149 env: &Option<std::collections::HashMap<String, String>>,
150) -> (Option<String>, Option<Vec<(String, String)>>) {
151 let base_path = if env.as_ref().map_or(true, |e| !e.contains_key("PATH")) {
153 std::env::var("PATH").ok()
154 } else {
155 env.as_ref().and_then(|e| e.get("PATH").cloned())
156 };
157
158 let final_path = base_path.map(|path| {
160 #[cfg(target_os = "windows")]
161 let path = {
162 if let Ok(appdata) = std::env::var("APPDATA") {
163 let npm_path = format!(r"{}\npm", appdata);
164 if !path.contains(&npm_path) {
165 format!("{};{}", path, npm_path)
166 } else {
167 path
168 }
169 } else {
170 tracing::warn!("Windows: APPDATA not found, skipping npm global bin");
171 path
172 }
173 };
174 ensure_runtime_path(&path)
175 });
176
177 let filtered_env = env.as_ref().map(|vars| {
179 vars.iter()
180 .filter(|(k, _)| k.as_str() != "PATH")
181 .map(|(k, v)| (k.clone(), v.clone()))
182 .collect()
183 });
184
185 (final_path, filtered_env)
186}
187
188#[cfg(unix)]
211#[macro_export]
212macro_rules! wrap_process_v8 {
213 ($cmd:expr) => {
214 {
215 use process_wrap::tokio::ProcessGroup;
216 $cmd.wrap(ProcessGroup::leader());
217 }
218 };
219}
220
221#[cfg(windows)]
222#[macro_export]
223macro_rules! wrap_process_v8 {
224 ($cmd:expr) => {
225 {
226 use process_wrap::tokio::{CreationFlags, JobObject};
227 use windows::Win32::System::Threading::CREATE_NO_WINDOW;
228 $cmd.wrap(CreationFlags(CREATE_NO_WINDOW));
229 $cmd.wrap(JobObject);
230 }
231 };
232}
233
234#[cfg(unix)]
257#[macro_export]
258macro_rules! wrap_process_v9 {
259 ($cmd:expr) => {
260 {
261 use process_wrap::tokio::ProcessGroup;
262 $cmd.wrap(ProcessGroup::leader());
263 }
264 };
265}
266
267#[cfg(windows)]
268#[macro_export]
269macro_rules! wrap_process_v9 {
270 ($cmd:expr) => {
271 {
272 use process_wrap::tokio::{CreationFlags, JobObject};
273 use windows::Win32::System::Threading::CREATE_NO_WINDOW;
274 $cmd.wrap(CreationFlags(CREATE_NO_WINDOW));
275 $cmd.wrap(JobObject);
276 }
277 };
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn test_check_windows_command_non_windows() {
286 check_windows_command("npx some-server");
288 check_windows_command("test.cmd");
289 }
290
291 #[test]
292 fn test_ensure_runtime_path_no_env() {
293 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
295 let result = ensure_runtime_path("/usr/bin:/usr/local/bin");
296 assert_eq!(result, "/usr/bin:/usr/local/bin");
297 }
298
299 #[test]
300 fn test_ensure_runtime_path_prepend() {
301 unsafe {
302 std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
303 }
304 let result = ensure_runtime_path("/usr/bin:/usr/local/bin");
305 assert_eq!(result, "/app/node/bin:/app/uv/bin:/usr/bin:/usr/local/bin");
306 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
307 }
308
309 #[test]
310 fn test_ensure_runtime_path_dedup() {
311 unsafe {
313 std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
314 }
315 let result =
316 ensure_runtime_path("/app/node/bin:/opt/homebrew/bin:/usr/bin");
317 assert_eq!(
318 result,
319 "/app/node/bin:/app/uv/bin:/opt/homebrew/bin:/usr/bin"
320 );
321 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
322 }
323
324 #[test]
325 fn test_ensure_runtime_path_all_present() {
326 unsafe {
328 std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
329 }
330 let result =
331 ensure_runtime_path("/app/uv/bin:/usr/bin:/app/node/bin");
332 assert_eq!(result, "/app/node/bin:/app/uv/bin:/usr/bin");
333 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
334 }
335
336 #[test]
337 fn test_ensure_runtime_path_double_node() {
338 unsafe {
340 std::env::set_var(
341 "NUWAX_APP_RUNTIME_PATH",
342 "/app/node/bin:/app/uv/bin:/app/debug",
343 );
344 }
345 let result = ensure_runtime_path(
346 "/app/node/bin:/app/node/bin:/app/uv/bin:/app/debug:/opt/homebrew/bin",
347 );
348 assert_eq!(
349 result,
350 "/app/node/bin:/app/uv/bin:/app/debug:/opt/homebrew/bin"
351 );
352 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
353 }
354}