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
365 #[test]
366 fn test_check_windows_command_non_windows() {
367 check_windows_command("npx some-server");
369 check_windows_command("test.cmd");
370 }
371
372 #[test]
373 fn test_ensure_runtime_path_no_env() {
374 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
376 let result = ensure_runtime_path("/usr/bin:/usr/local/bin");
377 assert_eq!(result, "/usr/bin:/usr/local/bin");
378 }
379
380 #[test]
381 fn test_ensure_runtime_path_prepend() {
382 unsafe {
383 std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
384 }
385 let result = ensure_runtime_path("/usr/bin:/usr/local/bin");
386 assert_eq!(result, "/app/node/bin:/app/uv/bin:/usr/bin:/usr/local/bin");
387 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
388 }
389
390 #[test]
391 fn test_ensure_runtime_path_dedup() {
392 unsafe {
394 std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
395 }
396 let result = ensure_runtime_path("/app/node/bin:/opt/homebrew/bin:/usr/bin");
397 assert_eq!(
398 result,
399 "/app/node/bin:/app/uv/bin:/opt/homebrew/bin:/usr/bin"
400 );
401 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
402 }
403
404 #[test]
405 fn test_ensure_runtime_path_all_present() {
406 unsafe {
408 std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
409 }
410 let result = ensure_runtime_path("/app/uv/bin:/usr/bin:/app/node/bin");
411 assert_eq!(result, "/app/node/bin:/app/uv/bin:/usr/bin");
412 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
413 }
414
415 #[test]
416 fn test_ensure_runtime_path_double_node() {
417 unsafe {
419 std::env::set_var(
420 "NUWAX_APP_RUNTIME_PATH",
421 "/app/node/bin:/app/uv/bin:/app/debug",
422 );
423 }
424 let result = ensure_runtime_path(
425 "/app/node/bin:/app/node/bin:/app/uv/bin:/app/debug:/opt/homebrew/bin",
426 );
427 assert_eq!(
428 result,
429 "/app/node/bin:/app/uv/bin:/app/debug:/opt/homebrew/bin"
430 );
431 unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
432 }
433}