#[cfg(windows)]
use tracing::{info, warn};
#[cfg(windows)]
pub fn check_windows_command(command: &str) {
use std::path::Path;
let cmd_ext = Path::new(command)
.extension()
.and_then(|e| e.to_str())
.map(|s| s.to_ascii_lowercase());
match cmd_ext.as_deref() {
Some("cmd" | "bat") => {
warn!(
"[MCP] Windows detected .cmd/.bat command: {} - CMD window may pop up!",
command
);
warn!(
"[MCP] It is recommended to use node.exe to run the JS file directly, or use the full path in the configuration"
);
}
None => {
if command.contains("npx") {
warn!(
"[MCP] Windows detects npx command: {} - CMD window may pop up!",
command
);
warn!("[MCP] It is recommended to use node.exe to run JS files directly");
}
}
_ => {
info!("[MCP] Windows detected command format: {}", command);
}
}
}
#[cfg(not(windows))]
pub fn check_windows_command(_command: &str) {
}
#[cfg(target_os = "windows")]
pub fn resolve_windows_command(command: &str) -> String {
use std::path::Path;
if Path::new(command).extension().is_some() {
return command.to_string();
}
if Path::new(command).is_absolute() {
return command.to_string();
}
let path_env = match std::env::var("PATH") {
Ok(p) => p,
Err(_) => return command.to_string(),
};
let extensions = [".cmd", ".exe", ".bat", ".ps1"];
for dir in path_env.split(';') {
let dir = dir.trim();
if dir.is_empty() {
continue;
}
for ext in &extensions {
let full_path = Path::new(dir).join(format!("{}{}", command, ext));
if full_path.exists() {
tracing::debug!(
"[MCP] Windows command analysis: {} -> {}",
command,
full_path.display()
);
return format!("{}{}", command, ext);
}
}
}
command.to_string()
}
#[cfg(not(target_os = "windows"))]
pub fn resolve_windows_command(command: &str) -> String {
command.to_string()
}
pub fn ensure_runtime_path(path: &str) -> String {
if let Ok(runtime_path) = std::env::var("NUWAX_APP_RUNTIME_PATH") {
let runtime_path = runtime_path.trim();
if !runtime_path.is_empty() {
let sep = if cfg!(windows) { ";" } else { ":" };
let runtime_segments: Vec<&str> =
runtime_path.split(sep).filter(|s| !s.is_empty()).collect();
let existing_segments: Vec<&str> = path
.split(sep)
.filter(|s| !s.is_empty() && !runtime_segments.contains(s))
.collect();
let merged: Vec<&str> = runtime_segments
.iter()
.copied()
.chain(existing_segments)
.collect();
let result = merged.join(sep);
if result != path {
tracing::info!(
"[ProcessCompat] Front-end application built-in runtime to PATH: {}",
runtime_path
);
}
return result;
}
}
path.to_string()
}
#[cfg(unix)]
#[macro_export]
macro_rules! wrap_process_v8 {
($cmd:expr) => {{
use process_wrap::tokio::ProcessGroup;
$cmd.wrap(ProcessGroup::leader());
}};
}
#[cfg(windows)]
#[macro_export]
macro_rules! wrap_process_v8 {
($cmd:expr) => {{
use process_wrap::tokio::{CreationFlags, JobObject};
use windows::Win32::System::Threading::{CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW};
$cmd.wrap(CreationFlags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP));
$cmd.wrap(JobObject);
}};
}
#[cfg(unix)]
#[macro_export]
macro_rules! wrap_process_v9 {
($cmd:expr) => {{
use process_wrap::tokio::ProcessGroup;
$cmd.wrap(ProcessGroup::leader());
}};
}
#[cfg(windows)]
#[macro_export]
macro_rules! wrap_process_v9 {
($cmd:expr) => {{
use process_wrap::tokio::{CreationFlags, JobObject};
use windows::Win32::System::Threading::{CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW};
$cmd.wrap(CreationFlags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP));
$cmd.wrap(JobObject);
}};
}
pub fn spawn_stderr_reader<T>(stderr: T, service_name: String) -> tokio::task::JoinHandle<()>
where
T: tokio::io::AsyncRead + Unpin + Send + 'static,
{
tokio::spawn(async move {
use tokio::io::{AsyncBufReadExt, BufReader};
let mut reader = BufReader::new(stderr);
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line).await {
Ok(0) => {
tracing::debug!("[Subprocess stderr][{}] End of read (EOF)", service_name);
break;
}
Ok(_) => {
let trimmed = line.trim();
if !trimmed.is_empty() {
tracing::warn!("[child process stderr][{}] {}", service_name, trimmed);
}
}
Err(e) => {
tracing::debug!("[Subprocess stderr][{}] Read error: {}", service_name, e);
break;
}
}
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_windows_command_non_windows() {
check_windows_command("npx some-server");
check_windows_command("test.cmd");
}
#[test]
fn test_ensure_runtime_path_no_env() {
unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
let result = ensure_runtime_path("/usr/bin:/usr/local/bin");
assert_eq!(result, "/usr/bin:/usr/local/bin");
}
#[test]
fn test_ensure_runtime_path_prepend() {
unsafe {
std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
}
let result = ensure_runtime_path("/usr/bin:/usr/local/bin");
assert_eq!(result, "/app/node/bin:/app/uv/bin:/usr/bin:/usr/local/bin");
unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
}
#[test]
fn test_ensure_runtime_path_dedup() {
unsafe {
std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
}
let result = ensure_runtime_path("/app/node/bin:/opt/homebrew/bin:/usr/bin");
assert_eq!(
result,
"/app/node/bin:/app/uv/bin:/opt/homebrew/bin:/usr/bin"
);
unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
}
#[test]
fn test_ensure_runtime_path_all_present() {
unsafe {
std::env::set_var("NUWAX_APP_RUNTIME_PATH", "/app/node/bin:/app/uv/bin");
}
let result = ensure_runtime_path("/app/uv/bin:/usr/bin:/app/node/bin");
assert_eq!(result, "/app/node/bin:/app/uv/bin:/usr/bin");
unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
}
#[test]
fn test_ensure_runtime_path_double_node() {
unsafe {
std::env::set_var(
"NUWAX_APP_RUNTIME_PATH",
"/app/node/bin:/app/uv/bin:/app/debug",
);
}
let result = ensure_runtime_path(
"/app/node/bin:/app/node/bin:/app/uv/bin:/app/debug:/opt/homebrew/bin",
);
assert_eq!(
result,
"/app/node/bin:/app/uv/bin:/app/debug:/opt/homebrew/bin"
);
unsafe { std::env::remove_var("NUWAX_APP_RUNTIME_PATH") };
}
}