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 detected .cmd/.bat command: {} - CMD window may pop up!",
68 command
69 );
70 warn!(
71 "[MCP] It is recommended to use node.exe to run the JS file directly, or use the full path in the configuration"
72 );
73 }
74 None => {
75 if command.contains("npx") {
77 warn!(
78 "[MCP] Windows detects npx command: {} - CMD window may pop up!",
79 command
80 );
81 warn!("[MCP] It is recommended to use node.exe to run JS files directly");
82 }
83 }
84 _ => {
85 info!("[MCP] Windows detected command format: {}", command);
86 }
87 }
88}
89
90#[cfg(not(windows))]
92pub fn check_windows_command(_command: &str) {
93 }
95
96#[cfg(target_os = "windows")]
119pub fn resolve_windows_command(command: &str) -> String {
120 use std::path::Path;
121
122 if Path::new(command).extension().is_some() {
124 return command.to_string();
125 }
126
127 if Path::new(command).is_absolute() {
129 return command.to_string();
130 }
131
132 let path_env = match std::env::var("PATH") {
134 Ok(p) => p,
135 Err(_) => return command.to_string(),
136 };
137
138 let extensions = [".cmd", ".exe", ".bat", ".ps1"];
140
141 for dir in path_env.split(';') {
143 let dir = dir.trim();
144 if dir.is_empty() {
145 continue;
146 }
147
148 for ext in &extensions {
150 let full_path = Path::new(dir).join(format!("{}{}", command, ext));
151 if full_path.exists() {
152 tracing::debug!(
153 "[MCP] Windows command analysis: {} -> {}",
154 command,
155 full_path.display()
156 );
157 return format!("{}{}", command, ext);
159 }
160 }
161 }
162
163 command.to_string()
165}
166
167#[cfg(not(target_os = "windows"))]
169pub fn resolve_windows_command(command: &str) -> String {
170 command.to_string()
171}
172
173pub fn ensure_runtime_path(path: &str) -> String {
184 if let Ok(runtime_path) = std::env::var("NUWAX_APP_RUNTIME_PATH") {
185 let runtime_path = runtime_path.trim();
186 if !runtime_path.is_empty() {
187 let sep = if cfg!(windows) { ";" } else { ":" };
188
189 let runtime_segments: Vec<&str> =
191 runtime_path.split(sep).filter(|s| !s.is_empty()).collect();
192
193 let existing_segments: Vec<&str> = path
195 .split(sep)
196 .filter(|s| !s.is_empty() && !runtime_segments.contains(s))
197 .collect();
198
199 let merged: Vec<&str> = runtime_segments
200 .iter()
201 .copied()
202 .chain(existing_segments)
203 .collect();
204
205 let result = merged.join(sep);
206 if result != path {
207 tracing::info!(
208 "[ProcessCompat] Front-end application built-in runtime to PATH: {}",
209 runtime_path
210 );
211 }
212 return result;
213 }
214 }
215 path.to_string()
216}
217
218#[cfg(unix)]
241#[macro_export]
242macro_rules! wrap_process_v8 {
243 ($cmd:expr) => {{
244 use process_wrap::tokio::ProcessGroup;
245 $cmd.wrap(ProcessGroup::leader());
246 }};
247}
248
249#[cfg(windows)]
250#[macro_export]
251macro_rules! wrap_process_v8 {
252 ($cmd:expr) => {{
253 use process_wrap::tokio::{CreationFlags, JobObject};
254 use windows::Win32::System::Threading::{CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW};
255 $cmd.wrap(CreationFlags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP));
256 $cmd.wrap(JobObject);
257 }};
258}
259
260#[cfg(unix)]
283#[macro_export]
284macro_rules! wrap_process_v9 {
285 ($cmd:expr) => {{
286 use process_wrap::tokio::ProcessGroup;
287 $cmd.wrap(ProcessGroup::leader());
288 }};
289}
290
291#[cfg(windows)]
292#[macro_export]
293macro_rules! wrap_process_v9 {
294 ($cmd:expr) => {{
295 use process_wrap::tokio::{CreationFlags, JobObject};
296 use windows::Win32::System::Threading::{CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW};
297 $cmd.wrap(CreationFlags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP));
298 $cmd.wrap(JobObject);
299 }};
300}
301
302pub fn spawn_stderr_reader<T>(stderr: T, service_name: String) -> tokio::task::JoinHandle<()>
330where
331 T: tokio::io::AsyncRead + Unpin + Send + 'static,
332{
333 tokio::spawn(async move {
334 use tokio::io::{AsyncBufReadExt, BufReader};
335
336 let mut reader = BufReader::new(stderr);
337 let mut line = String::new();
338 loop {
339 line.clear();
340 match reader.read_line(&mut line).await {
341 Ok(0) => {
342 tracing::debug!("[Subprocess stderr][{}] End of read (EOF)", service_name);
344 break;
345 }
346 Ok(_) => {
347 let trimmed = line.trim();
348 if !trimmed.is_empty() {
349 tracing::warn!("[child process stderr][{}] {}", service_name, trimmed);
350 }
351 }
352 Err(e) => {
353 tracing::debug!("[Subprocess stderr][{}] Read error: {}", service_name, e);
354 break;
355 }
356 }
357 }
358 })
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364 use std::sync::{Mutex, OnceLock};
365
366 fn env_lock() -> &'static Mutex<()> {
369 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
370 LOCK.get_or_init(|| Mutex::new(()))
371 }
372
373 #[test]
374 fn test_check_windows_command_non_windows() {
375 check_windows_command("npx some-server");
377 check_windows_command("test.cmd");
378 }
379
380 #[test]
381 fn test_ensure_runtime_path_no_env() {
382 let _env = env_lock().lock().unwrap_or_else(|e| e.into_inner());
383 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
385 let result = ensure_runtime_path("/usr/bin:/usr/local/bin");
386 assert_eq!(result, "/usr/bin:/usr/local/bin");
387 }
388
389 #[test]
390 fn test_ensure_runtime_path_prepend() {
391 let _env = env_lock().lock().unwrap_or_else(|e| e.into_inner());
392 unsafe {
393 std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
394 }
395 let result = ensure_runtime_path("/usr/bin:/usr/local/bin");
396 assert_eq!(result, "/app/node/bin:/app/uv/bin:/usr/bin:/usr/local/bin");
397 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
398 }
399
400 #[test]
401 fn test_ensure_runtime_path_dedup() {
402 let _env = env_lock().lock().unwrap_or_else(|e| e.into_inner());
403 unsafe {
405 std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
406 }
407 let result = ensure_runtime_path("/app/node/bin:/opt/homebrew/bin:/usr/bin");
408 assert_eq!(
409 result,
410 "/app/node/bin:/app/uv/bin:/opt/homebrew/bin:/usr/bin"
411 );
412 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
413 }
414
415 #[test]
416 fn test_ensure_runtime_path_all_present() {
417 let _env = env_lock().lock().unwrap_or_else(|e| e.into_inner());
418 unsafe {
420 std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
421 }
422 let result = ensure_runtime_path("/app/uv/bin:/usr/bin:/app/node/bin");
423 assert_eq!(result, "/app/node/bin:/app/uv/bin:/usr/bin");
424 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
425 }
426
427 #[test]
428 fn test_ensure_runtime_path_double_node() {
429 let _env = env_lock().lock().unwrap_or_else(|e| e.into_inner());
430 unsafe {
432 std::env::set_var(
433 "NUWAX_APP_RUNTIME_PATH",
434 "/app/node/bin:/app/uv/bin:/app/debug",
435 );
436 }
437 let result = ensure_runtime_path(
438 "/app/node/bin:/app/node/bin:/app/uv/bin:/app/debug:/opt/homebrew/bin",
439 );
440 assert_eq!(
441 result,
442 "/app/node/bin:/app/uv/bin:/app/debug:/opt/homebrew/bin"
443 );
444 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
445 }
446}