Skip to content

Launch Windows executables directly in stdio transport - #1839

Open
jozkee wants to merge 3 commits into
mainfrom
stdio_command_with_spaces
Open

Launch Windows executables directly in stdio transport#1839
jozkee wants to merge 3 commits into
mainfrom
stdio_command_with_spaces

Conversation

@jozkee

@jozkee jozkee commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Problem

On Windows, StdioClientTransport currently wraps every command other than an explicit cmd.exe invocation with:
cmd.exe /c <command> <arguments>
The wrapper allows commands such as npx to work because cmd.exe searches PATH and PATHEXT and resolves npx to npx.cmd. However, it also unnecessarily routes executables through cmd.exe.
This wrapper introduces an additional command-line parsing layer with behavior that differs from normal Windows argv parsing.

ProcessStartInfo.ArgumentList passes cmd.exe /c a correctly quoted executable path and argument, but cmd's legacy quote handling strips the outer quotes, reparses the command line, and mistakes the path prefix ending at the first space—such as C:\Program—for the executable. Although the /s approach explored in #1703 can avoid this, it requires constructing the entire command line manually and owning all cmd-specific quoting and escaping logic.

Discarded alternative: Resolve the filename before starting the process

I implemented and iterated on this approach in https://github.com/jozkee/csharp-sdk/pull/4/changes#diff-3fc7a658a90921218be1ac3af08dcc12db8d1ba201b9cb9f186dfb0c6b7cab21.

The TypeScript SDK's stdio transport calls cross-spawn, which eventually calls the Node which implementation; the Python SDK's Windows command helper calls shutil.which; and .NET Process.Start on Unix performs its own PATH resolution before calling execve. All three therefore perform some form of filename resolution before executing the child. In contrast, .NET's Windows implementation performs no managed filename resolution when UseShellExecute is false: it builds a command line from the supplied FileName and arguments and passes it to CreateProcess, whose lookup can search PATH but does not expand PATHEXT.

The reference implementations differ in their treatment of empty or quoted PATH entries, current-directory participation, deduplication, default PATHEXT values, and commands that already have an extension. Those differences reinforced that reproducing a complete resolver in the SDK would add significant complexity and platform-specific behavior.

https://github.com/npm/node-which/blob/v2.0.2/which.js
https://github.com/python/cpython/blob/v3.14.0/Lib/shutil.py
https://github.com/dotnet/runtime/blob/fea92eb70a2b115e3c8c73aba01eea615a82eee2/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessUtils.Unix.cs#L268-L330

This PR

This PR takes a deliberately surgical approach, suggested in #1703 (comment).
Explicit .exe and .com commands are launched directly when they are rooted or exist relative to the parent process's current directory, consistent with the relevant part of .NET's Unix resolution behavior.
Directly launched executables receive their arguments through the normal argv path, without cmd-specific escaping.
Bare commands and script files, such as .cmd and .bat files, continue through the existing cmd.exe /c path.
Existing PATH, PATHEXT, batch-file, and WorkingDirectory behavior is therefore preserved for commands that require shell resolution.
This fixes paths containing spaces without introducing a parallel command resolver or changing how common commands such as npx are located.

Fixes #1601

cc @yayayouyou

Bypass cmd.exe for rooted or existing .exe and .com commands so their arguments are passed without shell escaping, while preserving cmd.exe handling for shell-resolved commands.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jozkee and others added 2 commits August 27, 2026 10:40
Stage the complete test server payload so runtime-specific assets and .NET Framework binding redirects remain available when the executable is renamed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

StdioClientTransportOptions is failing with space in Command

1 participant