Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion electron/bridges/terminalBridge.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,52 @@ function getLocalShellArgs(shellPath) {
return [];
}

function isWslExecutable(shellPath) {
if (process.platform !== "win32" || typeof shellPath !== "string") return false;
return /(?:^|[\\/])wsl(?:\.exe)?$/i.test(shellPath.trim());
}

function getWslLaunchArgs(shellPath, shellArgs, hasExplicitCwd) {
const args = Array.isArray(shellArgs) ? [...shellArgs] : [];
// Without --cd, wsl.exe translates the parent Windows cwd. That can fail
// before the Linux shell starts (for example, when the Windows home is not
// mounted or accessible to the selected distro). Start at Linux $HOME unless
// the caller deliberately supplied a working directory or --cd option.
if (!isWslExecutable(shellPath) || hasExplicitCwd) {
return args;
}

// WSL consumes an optional legacy distro GUID, then a home-directory ~,
// before parsing normal options. Keep both in their original positions.
const firstOptionIndex = /^\{?[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}\}?$/i.test(args[0] || "") ? 1 : 0;
if (args[firstOptionIndex] === "~") return args;

// The tokens following --/--exec/-e, or the first bare command, are passed to
// Linux verbatim. Account for WSL options with a separate value before
// locating that boundary, then insert --cd before it rather than accidentally
// passing --cd to the shell or command.
let commandIndex = -1;
for (let index = firstOptionIndex; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--" || arg === "--exec" || arg === "-e" || !arg.startsWith("-")) {
commandIndex = index;
break;
}
// Only WSL's directory option is explicit; a Linux command may itself
// accept --cd without changing the directory WSL starts in.
if (arg === "--cd" || arg.startsWith("--cd=")) return args;
if (
arg === "--distribution" || arg === "-d" || arg === "--distribution-id" ||
arg === "--user" || arg === "-u" || arg === "--shell-type"
) {
index += 1;
}
}
const insertAt = commandIndex === -1 ? args.length : commandIndex;
args.splice(insertAt, 0, "--cd", "~");
return args;
}

const isUtf8Locale = (value) => typeof value === "string" && /utf-?8/i.test(value);

const isEmptyLocale = (value) => {
Expand Down Expand Up @@ -896,7 +942,12 @@ function startLocalSession(event, payload) {
}
}
const shell = normalizeExecutablePath(resolvedShell) || defaultShell;
const shellArgs = resolvedArgs ?? getLocalShellArgs(shell);
const requestedCwd = typeof payload?.cwd === "string" && payload.cwd.trim().length > 0;
const shellArgs = getWslLaunchArgs(
shell,
resolvedArgs ?? getLocalShellArgs(shell),
requestedCwd,
);
const shellKind = detectShellKind(shell);
const { buildTerminalProcessEnv } = require("./httpNetworkProxyBridge.cjs");
const env = applyLocaleDefaults({
Expand Down Expand Up @@ -2536,6 +2587,7 @@ module.exports = {
registerHandlers,
findExecutable,
getDefaultLocalShell,
getWslLaunchArgs,
startLocalSession,
startTelnetSession,
startMoshSession,
Expand Down
87 changes: 87 additions & 0 deletions electron/bridges/terminalBridge.outputFlood.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,93 @@ function loadBridgeWithFakes(spawns, sentries) {
}
}

test("WSL launch arguments use Linux home unless a working directory is explicit", () => {
const bridge = loadBridgeWithFakes([], []);
const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");

try {
Object.defineProperty(process, "platform", { ...platformDescriptor, value: "win32" });
assert.deepEqual(
bridge.getWslLaunchArgs("C:\\Windows\\System32\\wsl.exe", ["-d", "Ubuntu"], false),
["-d", "Ubuntu", "--cd", "~"],
);
assert.deepEqual(
bridge.getWslLaunchArgs("C:\\Windows\\System32\\wsl.exe", ["-d", "Ubuntu"], true),
["-d", "Ubuntu"],
);
assert.deepEqual(
bridge.getWslLaunchArgs(
"C:\\Windows\\System32\\wsl.exe",
["-d", "Ubuntu", "--exec", "zsh", "-l"],
false,
),
["-d", "Ubuntu", "--cd", "~", "--exec", "zsh", "-l"],
);
} finally {
Object.defineProperty(process, "platform", platformDescriptor);
}
});

test("WSL default directory stays outside the Linux command and option values", () => {
const bridge = loadBridgeWithFakes([], []);
const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
const distroGuid = "{01234567-89ab-cdef-0123-456789abcdef}";
const cases = [
[[distroGuid], [distroGuid, "--cd", "~"]],
[[distroGuid, "~"], [distroGuid, "~"]],
[[distroGuid, "--", "zsh"], [distroGuid, "--cd", "~", "--", "zsh"]],
[["~"], ["~"]],
[["~", "-d", "Ubuntu", "--", "zsh"], ["~", "-d", "Ubuntu", "--", "zsh"]],
[["-d", "Ubuntu", "--", "zsh", "-l"], ["-d", "Ubuntu", "--cd", "~", "--", "zsh", "-l"]],
[["-e", "zsh", "-l"], ["--cd", "~", "-e", "zsh", "-l"]],
[["--shell-type", "login", "zsh"], ["--shell-type", "login", "--cd", "~", "zsh"]],
[["--distribution-id", "{distro-id}"], ["--distribution-id", "{distro-id}", "--cd", "~"]],
[["--user", "root", "echo", "--cd"], ["--user", "root", "--cd", "~", "echo", "--cd"]],
[["--exec", "echo", "--cd=/tmp"], ["--cd", "~", "--exec", "echo", "--cd=/tmp"]],
[["--cd", "/tmp", "--", "zsh"], ["--cd", "/tmp", "--", "zsh"]],
[["--cd=/tmp", "-e", "zsh"], ["--cd=/tmp", "-e", "zsh"]],
];
try {
Object.defineProperty(process, "platform", { ...platformDescriptor, value: "win32" });
for (const [args, expected] of cases) {
const input = Object.freeze([...args]);
assert.deepEqual(bridge.getWslLaunchArgs("wsl.exe", input, false), expected, JSON.stringify(args));
assert.deepEqual(bridge.getWslLaunchArgs("wsl.exe", input, true), args);
assert.deepEqual(bridge.getWslLaunchArgs("powershell.exe", input, false), args);
}
Object.defineProperty(process, "platform", { ...platformDescriptor, value: "linux" });
assert.deepEqual(bridge.getWslLaunchArgs("/usr/bin/wsl", [], false), []);
} finally {
Object.defineProperty(process, "platform", platformDescriptor);
}
});

test("WSL local sessions pass the home directory option to the spawned process", () => {
const spawns = [];
const bridge = loadBridgeWithFakes(spawns, []);
const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
try {
Object.defineProperty(process, "platform", { ...platformDescriptor, value: "win32" });
bridge.init({
sessions: new Map(),
electronModule: { webContents: { fromId: () => ({ send() {} }) } },
});
const shellArgs = ["-d", "Ubuntu", "--", "zsh", "-l"];
const payload = { shell: "C:\\Windows\\System32\\wsl.exe", shellArgs };
bridge.startLocalSession({ sender: { id: 7 } }, { ...payload, sessionId: "wsl-home" });
assert.deepEqual(spawns[0].spawnArgs[1], ["-d", "Ubuntu", "--cd", "~", "--", "zsh", "-l"]);
assert.deepEqual(shellArgs, ["-d", "Ubuntu", "--", "zsh", "-l"]);
bridge.startLocalSession(
{ sender: { id: 7 } },
{ ...payload, sessionId: "wsl-explicit-cwd", cwd: process.cwd() },
);
assert.deepEqual(spawns[1].spawnArgs[1], shellArgs);
assert.equal(spawns[1].spawnArgs[2].cwd, process.cwd());
} finally {
Object.defineProperty(process, "platform", platformDescriptor);
}
});

test("Windows local terminals enable the bundled ConPTY implementation required for clear", () => {
const spawns = [];
const sentries = [];
Expand Down
Loading