Skip to content

Commit e2e7f3f

Browse files
committed
🤖 fix: use base64 encoding instead of stdin for PowerShell WSL wrapper
Stdin piping to PowerShell didn't work reliably ($input and [Console]::In both failed). Instead, use base64 encoding to embed the script safely in the PowerShell command line: 1. Base64 encode the bash script (completely avoids escaping issues) 2. PowerShell decodes it with [System.Text.Encoding]::UTF8.GetString() 3. Echo the decoded script and pipe to WSL bash This approach is robust because base64 strings only contain [A-Za-z0-9+/=] which have no special meaning in PowerShell. Also removes the now-unused stdin field from SpawnConfig.
1 parent 3a51bab commit e2e7f3f

File tree

4 files changed

+17
-54
lines changed

4 files changed

+17
-54
lines changed

src/node/runtime/LocalBaseRuntime.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ export abstract class LocalBaseRuntime implements Runtime {
7474
command: bashCommand,
7575
args: bashArgs,
7676
cwd: spawnCwd,
77-
stdin: stdinContent,
7877
} = getPreferredSpawnConfig(command, cwd);
7978

8079
// If niceness is specified on Unix/Linux, spawn nice directly to avoid escaping issues
@@ -104,12 +103,6 @@ export abstract class LocalBaseRuntime implements Runtime {
104103
windowsHide: true,
105104
});
106105

107-
// Write stdin content if provided (used for WSL via PowerShell to avoid escaping issues)
108-
if (stdinContent) {
109-
childProcess.stdin?.write(stdinContent);
110-
childProcess.stdin?.end();
111-
}
112-
113106
// Wrap in DisposableProcess for automatic cleanup
114107
const disposable = new DisposableProcess(childProcess);
115108

@@ -388,13 +381,11 @@ export abstract class LocalBaseRuntime implements Runtime {
388381
command: bashCommand,
389382
args: bashArgs,
390383
cwd: spawnCwd,
391-
stdin: stdinContent,
392384
} = getPreferredSpawnConfig(`"${hookPath}"`, workspacePath);
393385

394386
const proc = spawn(bashCommand, bashArgs, {
395387
cwd: spawnCwd,
396-
// Use pipe for stdin if we need to send input (for WSL via PowerShell)
397-
stdio: [stdinContent ? "pipe" : "ignore", "pipe", "pipe"],
388+
stdio: ["ignore", "pipe", "pipe"],
398389
env: {
399390
...process.env,
400391
...getInitHookEnv(projectPath, runtimeType),
@@ -403,12 +394,6 @@ export abstract class LocalBaseRuntime implements Runtime {
403394
windowsHide: true,
404395
});
405396

406-
// Write stdin content if provided (used for WSL via PowerShell to avoid escaping issues)
407-
if (stdinContent && proc.stdin) {
408-
proc.stdin.write(stdinContent);
409-
proc.stdin.end();
410-
}
411-
412397
proc.stdout?.on("data", (data: Buffer) => {
413398
loggers.stdout.append(data.toString());
414399
});

src/node/services/bashExecutionService.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,6 @@ export class BashExecutionService {
128128
command: bashCommand,
129129
args: bashArgs,
130130
cwd: spawnCwd,
131-
stdin: stdinContent,
132131
} = getPreferredSpawnConfig(script, config.cwd);
133132

134133
// Windows doesn't have nice command, so just spawn bash directly
@@ -142,8 +141,7 @@ export class BashExecutionService {
142141
const child = spawn(spawnCommand, spawnArgs, {
143142
cwd: spawnCwd,
144143
env: this.createBashEnvironment(config.secrets),
145-
// Use pipe for stdin if we need to send input (for WSL via PowerShell)
146-
stdio: [stdinContent ? "pipe" : "ignore", "pipe", "pipe"],
144+
stdio: ["ignore", "pipe", "pipe"],
147145
// Spawn as detached process group leader to prevent zombie processes
148146
// When bash spawns background processes, detached:true allows killing
149147
// the entire group via process.kill(-pid)
@@ -152,12 +150,6 @@ export class BashExecutionService {
152150
windowsHide: true,
153151
});
154152

155-
// Write stdin content if provided (used for WSL via PowerShell to avoid escaping issues)
156-
if (stdinContent && child.stdin) {
157-
child.stdin.write(stdinContent);
158-
child.stdin.end();
159-
}
160-
161153
log.debug(`BashExecutionService: Spawned process with PID ${child.pid ?? "unknown"}`);
162154

163155
// Line-by-line streaming with incremental buffers

src/node/utils/disposableExec.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -129,19 +129,12 @@ class DisposableExec implements Disposable {
129129
export function execAsync(command: string): DisposableExec {
130130
// Wrap command in bash -c for consistent cross-platform behavior
131131
// For WSL, this also translates Windows paths to /mnt/... format
132-
const { command: bashCmd, args, stdin } = getPreferredSpawnConfig(command);
132+
const { command: bashCmd, args } = getPreferredSpawnConfig(command);
133133
const child = spawn(bashCmd, args, {
134-
// Use pipe for stdin if we need to send input (for WSL via PowerShell)
135-
stdio: [stdin ? "pipe" : "ignore", "pipe", "pipe"],
134+
stdio: ["ignore", "pipe", "pipe"],
136135
// Prevent console window from appearing on Windows
137136
windowsHide: true,
138137
});
139-
140-
// Write stdin content if provided (used for WSL via PowerShell to avoid escaping issues)
141-
if (stdin && child.stdin) {
142-
child.stdin.write(stdin);
143-
child.stdin.end();
144-
}
145138
const promise = new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
146139
let stdout = "";
147140
let stderr = "";

src/node/utils/main/bashPath.ts

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,6 @@ export interface SpawnConfig {
5454
args: string[];
5555
/** Working directory (translated for WSL if needed) */
5656
cwd?: string;
57-
/** Optional stdin to pipe to the process (used for WSL to avoid escaping issues) */
58-
stdin?: string;
5957
}
6058

6159
// ============================================================================
@@ -388,29 +386,24 @@ export function getSpawnConfig(runtime: BashRuntime, script: string, cwd?: strin
388386
const fullScript = cdPrefix + translatedScript;
389387

390388
// Try to use PowerShell to hide WSL console window
391-
// Pass the script via stdin to avoid PowerShell escaping issues with special chars
389+
// Use base64 encoding to completely avoid escaping issues with special chars
392390
const psPath = getPowerShellPath();
393391
if (psPath) {
394-
// Build WSL command to read script from stdin
395-
const wslCmd = runtime.distro ? `wsl -d ${runtime.distro} bash` : "wsl bash";
396-
397-
// PowerShell will receive the script via stdin and pipe it to WSL
398-
// -NoProfile: faster startup, avoids profile execution policy issues
399-
// -WindowStyle Hidden: hide console window
400-
// -Command: execute the piped command
401-
// Use [Console]::In.ReadToEnd() instead of $input because $input doesn't work
402-
// reliably with -Command (it's meant for pipeline input in functions/filters)
392+
// Base64 encode the script to avoid any PowerShell parsing issues
393+
// PowerShell will decode it and pipe to WSL bash
394+
const base64Script = Buffer.from(fullScript, "utf8").toString("base64");
395+
396+
// Build the PowerShell command that:
397+
// 1. Decodes the base64 script
398+
// 2. Pipes it to WSL bash via echo
399+
const wslArgs = runtime.distro ? `-d ${runtime.distro}` : "";
400+
const psCommand = `$s=[System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${base64Script}'));` +
401+
`echo $s | wsl ${wslArgs} bash`.trim();
402+
403403
return {
404404
command: psPath,
405-
args: [
406-
"-NoProfile",
407-
"-WindowStyle",
408-
"Hidden",
409-
"-Command",
410-
`[Console]::In.ReadToEnd() | ${wslCmd}`,
411-
],
405+
args: ["-NoProfile", "-WindowStyle", "Hidden", "-Command", psCommand],
412406
cwd: undefined, // cwd is embedded in the script
413-
stdin: fullScript,
414407
};
415408
}
416409

0 commit comments

Comments
 (0)