@@ -78,12 +78,23 @@ fn build_stdio_command(cmd: &str, args: &[String], cwd: Option<&Path>) -> Comman
7878 #[ cfg( windows) ]
7979 {
8080 if is_windows_batch_program ( & program) {
81+ use std:: os:: windows:: process:: CommandExt ;
82+
83+ // .cmd/.bat 无法被 CreateProcess 直接执行,需经 cmd.exe 转发。
84+ // /C 后的命令行必须用 raw_arg 原样传入:arg() 会按 MSVCRT 规则
85+ // 把内嵌引号转义成 `\"`,cmd.exe 不识别该转义,子进程瞬退,
86+ // stdin 写入报 os error 232(issue #205)。
87+ // /E:ON 保证命令扩展可用(`%%cd:~,` 防展开 hack 依赖它),
88+ // /V:OFF 关闭延迟展开,防止参数里的 `!VAR!` 被替换;
89+ // 均与 std `make_bat_command_line` 的 `/e:ON /v:OFF` 对齐。
8190 let mut command = Command :: new ( "cmd.exe" ) ;
8291 command
92+ . arg ( "/E:ON" )
93+ . arg ( "/V:OFF" )
8394 . arg ( "/D" )
8495 . arg ( "/S" )
85- . arg ( "/C" )
86- . arg ( windows_batch_command_line ( & program, args) ) ;
96+ . arg ( "/C" ) ;
97+ command . raw_arg ( windows_cmd_c_argument ( & program, args) ) ;
8798 return command;
8899 }
89100 }
@@ -93,27 +104,54 @@ fn build_stdio_command(cmd: &str, args: &[String], cwd: Option<&Path>) -> Comman
93104 command
94105}
95106
96- #[ cfg ( windows) ]
107+ #[ cfg_attr ( not ( windows) , allow ( dead_code ) ) ]
97108fn is_windows_batch_program ( path : & Path ) -> bool {
98109 path. extension ( )
99110 . and_then ( |ext| ext. to_str ( ) )
100111 . map ( |ext| ext. eq_ignore_ascii_case ( "cmd" ) || ext. eq_ignore_ascii_case ( "bat" ) )
101112 . unwrap_or ( false )
102113}
103114
104- #[ cfg( windows) ]
105- fn windows_batch_command_line ( program : & Path , args : & [ String ] ) -> String {
106- std:: iter:: once ( program. to_string_lossy ( ) . into_owned ( ) )
115+ /// 组装 `cmd.exe /S /C` 之后的整段命令行:外层再包一对引号,`/S` 语义下
116+ /// cmd 仅剥掉首尾引号,剩余部分按原样执行。
117+ #[ cfg_attr( not( windows) , allow( dead_code) ) ]
118+ fn windows_cmd_c_argument ( program : & Path , args : & [ String ] ) -> String {
119+ let line = std:: iter:: once ( program. to_string_lossy ( ) . into_owned ( ) )
107120 . chain ( args. iter ( ) . cloned ( ) )
108121 . map ( |value| windows_cmd_quote_arg ( & value) )
109122 . collect :: < Vec < _ > > ( )
110- . join ( " " )
123+ . join ( " " ) ;
124+ format ! ( "\" {line}\" " )
111125}
112126
113- #[ cfg( windows) ]
127+ /// 引号包裹单个参数,转义规则对齐 std `sys/args/windows.rs::append_bat_arg`:
128+ /// - 内嵌引号前的反斜杠补齐至 2n 再把引号翻倍(cmd.exe 不识别 `\"`);
129+ /// - 收尾引号前的尾部反斜杠同样翻倍,防止 `C:\dir\` 这类参数把闭合引号
130+ /// 转义掉、与后一个参数粘连;
131+ /// - `%`/`\r` 前插入 `%%cd:~,` no-op(yt-dlp hack,依赖 `/E:ON`),阻止
132+ /// `%VAR%` 被 cmd 当环境变量展开,子进程仍收到原文。
133+ #[ cfg_attr( not( windows) , allow( dead_code) ) ]
114134fn windows_cmd_quote_arg ( value : & str ) -> String {
115- let escaped = value. replace ( '"' , "\\ \" " ) ;
116- format ! ( "\" {escaped}\" " )
135+ let mut escaped = String :: with_capacity ( value. len ( ) + 2 ) ;
136+ escaped. push ( '"' ) ;
137+ let mut backslashes = 0usize ;
138+ for ch in value. chars ( ) {
139+ if ch == '\\' {
140+ backslashes += 1 ;
141+ } else {
142+ if ch == '"' {
143+ escaped. extend ( std:: iter:: repeat_n ( '\\' , backslashes) ) ;
144+ escaped. push ( '"' ) ;
145+ } else if ch == '%' || ch == '\r' {
146+ escaped. push_str ( "%%cd:~," ) ;
147+ }
148+ backslashes = 0 ;
149+ }
150+ escaped. push ( ch) ;
151+ }
152+ escaped. extend ( std:: iter:: repeat_n ( '\\' , backslashes) ) ;
153+ escaped. push ( '"' ) ;
154+ escaped
117155}
118156
119157#[ derive( Debug , Serialize ) ]
@@ -1970,4 +2008,78 @@ mod tests {
19702008 . expect ( "contender ensure eventually succeeds" ) ;
19712009 holder. join ( ) . expect ( "join holder" ) ;
19722010 }
2011+
2012+ #[ test]
2013+ fn detects_windows_batch_programs_by_extension ( ) {
2014+ assert ! ( is_windows_batch_program( Path :: new(
2015+ r"C:\Program Files\nodejs\npx.cmd"
2016+ ) ) ) ;
2017+ assert ! ( is_windows_batch_program( Path :: new( r"C:\tools\run.BAT" ) ) ) ;
2018+ assert ! ( !is_windows_batch_program( Path :: new(
2019+ r"C:\Program Files\nodejs\node.exe"
2020+ ) ) ) ;
2021+ assert ! ( !is_windows_batch_program( Path :: new( "npx" ) ) ) ;
2022+ }
2023+
2024+ #[ test]
2025+ fn windows_cmd_quote_arg_doubles_embedded_quotes ( ) {
2026+ // cmd.exe 不认 `\"` 转义,翻倍才能保持引号配对。
2027+ assert_eq ! ( windows_cmd_quote_arg( "-y" ) , r#""-y""# ) ;
2028+ assert_eq ! ( windows_cmd_quote_arg( r#"a"b"# ) , r#""a""b""# ) ;
2029+ assert_eq ! ( windows_cmd_quote_arg( "with space" ) , r#""with space""# ) ;
2030+ }
2031+
2032+ #[ test]
2033+ fn windows_cmd_quote_arg_doubles_backslashes_before_quotes ( ) {
2034+ // 内嵌引号前的反斜杠须补齐至 2n,重解析后还原为 n 个反斜杠 + 字面引号。
2035+ assert_eq ! ( windows_cmd_quote_arg( r#"a\"b"# ) , r#""a\\""b""# ) ;
2036+ // 尾部反斜杠若不翻倍会把闭合引号转义掉,与后一个参数粘连。
2037+ assert_eq ! ( windows_cmd_quote_arg( r"C:\data\" ) , r#""C:\data\\""# ) ;
2038+ // 非贴引号的反斜杠保持原样(路径分隔符不受影响)。
2039+ assert_eq ! ( windows_cmd_quote_arg( r"C:\a\b" ) , r#""C:\a\b""# ) ;
2040+ assert_eq ! ( windows_cmd_quote_arg( "" ) , r#""""# ) ;
2041+ }
2042+
2043+ #[ test]
2044+ fn windows_cmd_quote_arg_neutralizes_percent_expansion ( ) {
2045+ // `%%cd:~,` no-op 打断 %VAR% 配对,cmd 展开后子进程仍收到原文。
2046+ assert_eq ! ( windows_cmd_quote_arg( "%PATH%" ) , r#""%%cd:~,%PATH%%cd:~,%""# ) ;
2047+ assert_eq ! ( windows_cmd_quote_arg( "100%" ) , r#""100%%cd:~,%""# ) ;
2048+ assert_eq ! ( windows_cmd_quote_arg( "a\r b" ) , "\" a%%cd:~,\r b\" " ) ;
2049+ }
2050+
2051+ #[ test]
2052+ fn windows_cmd_c_argument_wraps_whole_line_for_slash_s ( ) {
2053+ // `/S` 语义:cmd 剥掉首尾引号后必须还原出可执行的完整命令行。
2054+ let program = Path :: new ( r"C:\Program Files\nodejs\npx.cmd" ) ;
2055+ let args = vec ! [ "-y" . to_string( ) , "@playwright/mcp" . to_string( ) ] ;
2056+ assert_eq ! (
2057+ windows_cmd_c_argument( program, & args) ,
2058+ r#"""C:\Program Files\nodejs\npx.cmd" "-y" "@playwright/mcp"""#
2059+ ) ;
2060+ }
2061+
2062+ #[ test]
2063+ fn windows_cmd_c_argument_without_args_still_quotes_program ( ) {
2064+ let program = Path :: new ( r"C:\tools\npx.cmd" ) ;
2065+ assert_eq ! (
2066+ windows_cmd_c_argument( program, & [ ] ) ,
2067+ r#"""C:\tools\npx.cmd"""#
2068+ ) ;
2069+ }
2070+
2071+ #[ test]
2072+ fn windows_cmd_c_argument_survives_trailing_backslash_arg ( ) {
2073+ // filesystem 类 MCP server 常见传法:目录参数带尾部反斜杠。
2074+ let program = Path :: new ( r"C:\Program Files\nodejs\npx.cmd" ) ;
2075+ let args = vec ! [
2076+ "-y" . to_string( ) ,
2077+ "@modelcontextprotocol/server-filesystem" . to_string( ) ,
2078+ r"C:\Users\me\docs\" . to_string( ) ,
2079+ ] ;
2080+ assert_eq ! (
2081+ windows_cmd_c_argument( program, & args) ,
2082+ r#"""C:\Program Files\nodejs\npx.cmd" "-y" "@modelcontextprotocol/server-filesystem" "C:\Users\me\docs\\"""#
2083+ ) ;
2084+ }
19732085}
0 commit comments