Skip to content

fix(xtask): launch non-exe commands through cmd.exe on Windows#79

Merged
akroshg merged 3 commits intomainfrom
fix_command_windows
Mar 7, 2026
Merged

fix(xtask): launch non-exe commands through cmd.exe on Windows#79
akroshg merged 3 commits intomainfrom
fix_command_windows

Conversation

@akroshg
Copy link
Copy Markdown
Contributor

@akroshg akroshg commented Mar 6, 2026

On Windows, CreateProcessW only resolves .exe files, so non-exe commands like .cmd/.bat scripts (e.g. pnpm) fail when spawned directly via Command::new.

This change updates spawn_child in xtask/src/process.rs to launch commands through cmd.exe /c on Windows, ensuring batch-script-based tools work correctly.

Changes

  • xtask/src/process.rs: Wrap command invocation in cmd /c on Windows (cfg!(windows)), while keeping direct execution on other platforms.

On Windows, CreateProcessW only resolves .exe files, so non-.exe
commands (e.g. .cmd/.bat scripts like pnpm) fail to spawn. Wrap
command invocations through cmd /c on Windows to resolve these
correctly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@akroshg
Copy link
Copy Markdown
Contributor Author

akroshg commented Mar 6, 2026

Attempt to fix #78

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a Windows-specific process-spawning failure in the xtask dev tooling. On Windows, CreateProcessW cannot resolve non-.exe commands such as .cmd/.bat scripts (e.g. pnpm). The fix wraps the spawned command in cmd.exe /c when cfg!(windows) is true, while leaving Unix behavior unchanged.

Changes:

  • xtask/src/process.rs: Restructures spawn_child so that on Windows the command is launched via cmd.exe /c <cmd> <args...>, and on non-Windows it continues to use Command::new(cmd) directly.

Comment thread xtask/src/process.rs Outdated
Comment on lines +35 to +43
let mut command = if cfg!(windows) {
let mut c = Command::new("cmd");
c.arg("/c").arg(cmd).args(args);
c
} else {
let mut c = Command::new(cmd);
c.args(args);
c
};
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When cmd.exe /c is used to wrap the command on Windows, the Child handle returned by command.spawn() refers to the cmd.exe process, not the actual target process (e.g. pnpm). As a result, send_graceful_stop sends CTRL_BREAK_EVENT to cmd.exe's process group ID (the cmd.exe PID), not to the real subprocess's group. Because cmd.exe /c spawns the real process in its own context and does not forward CTRL_BREAK_EVENT, the graceful shutdown mechanism (lines 229–238) silently fails: only cmd.exe is signalled, and the real long-running process (e.g. a pnpm dev server) is left running or must wait for the 3-second timeout to be force-killed.

A more robust approach for Windows is to use where.exe or std::env::var("PATHEXT") to locate the actual .cmd/.bat file and pass its full path to Command::new, or to use the which crate to resolve the executable. Alternatively, if cmd.exe /c must be kept, the process group setup and graceful-stop logic need to account for the extra cmd.exe wrapper layer.

Copilot uses AI. Check for mistakes.
Comment thread xtask/src/process.rs Outdated
Comment on lines +33 to +39
// On Windows, non-.exe commands (e.g. .cmd/.bat scripts like pnpm) must be
// launched through cmd.exe because CreateProcessW only resolves .exe files.
let mut command = if cfg!(windows) {
let mut c = Command::new("cmd");
c.arg("/c").arg(cmd).args(args);
c
} else {
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When cmd.exe /c is used, individual arguments in the args slice are passed as separate arg() calls, but cmd.exe reassembles them into a single command line string using its own quoting rules. If any argument or the cmd value contains spaces, special characters (&, |, >, <, ^, %, !), or quotes, cmd.exe may misinterpret them, leading to incorrect behavior or potential argument injection. The current callers only pass simple strings like "start:server", so this is not an immediate problem, but it is a latent hazard as the function is public and could be called with arbitrary arguments in the future. Consider documenting this limitation or using std::os::windows::process::CommandExt::raw_arg to bypass cmd.exe's re-quoting.

Suggested change
// On Windows, non-.exe commands (e.g. .cmd/.bat scripts like pnpm) must be
// launched through cmd.exe because CreateProcessW only resolves .exe files.
let mut command = if cfg!(windows) {
let mut c = Command::new("cmd");
c.arg("/c").arg(cmd).args(args);
c
} else {
// Spawn the requested command directly; on Windows we rely on PATH/PATHEXT
// resolution so that shell wrapper scripts (e.g. .cmd/.bat) are located
// without going through `cmd.exe`.
let mut command = {

Copilot uses AI. Check for mistakes.
@mohamedmansour
Copy link
Copy Markdown
Contributor

@copilot does this work on powershell terminals as well @akroshg

Copy link
Copy Markdown

Copilot AI commented Mar 6, 2026

@mohamedmansour I've opened a new pull request, #80, to work on those changes. Once the pull request is ready, I'll request review from you.

akroshg and others added 2 commits March 6, 2026 15:28
On Windows, CreateProcessW cannot launch .cmd/.bat scripts directly.
The previous approach wrapped all commands in cmd.exe /c, which broke
graceful shutdown (CTRL_BREAK_EVENT targeted cmd.exe, not the real
process) and introduced argument-injection risks.

Changes:
- Add build_command() helper in util.rs that uses the which crate to
  resolve executables on Windows: .cmd/.bat are wrapped in cmd.exe /c,
  while .exe files are launched directly.
- Introduce ManagedChild wrapper around std::process::Child that holds
  a Windows Job Object with KILL_ON_JOB_CLOSE, ensuring the entire
  process tree (including cmd.exe children) is terminated on drop.
- Apply build_command() to both spawn_child (process.rs) and
  run_command (util.rs) so all xtask command invocations resolve
  correctly on Windows.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@mohamedmansour mohamedmansour left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting :D

@akroshg akroshg merged commit 4d522e8 into main Mar 7, 2026
10 checks passed
@mohamedmansour mohamedmansour deleted the fix_command_windows branch March 7, 2026 02:10
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.

4 participants