This commit is contained in:
YOMXXX 2026-05-31 01:23:31 -04:00 committed by GitHub
commit a7111b0064
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 257 additions and 1 deletions

View file

@ -0,0 +1,70 @@
/**
* Open a validated absolute directory in the host platform's native file
* manager. Pulled out of the `shell:open-path` IPC handler so the routing
* decision (Electron's default opener vs. WSL Explorer pivot) can be unit
* tested without booting Electron.
*
* **WSL routing rationale (#1581).** On WSL Electron, `shell.openPath`
* delegates to xdg-open on Linux, and `xdg-open` typically routes
* `inode/directory` MIME through the default browser (Chrome on common WSL
* setups via `wslu`) rather than Explorer or a native Linux file manager.
* Route through `wslpath -w <dir>` + `explorer.exe <windows-path>` so the
* Windows host's Explorer opens the resolved folder, matching what the
* "Continue in CLI" flow promised users.
*
* Non-WSL Linux installs with proper `xdg-open` MIME associations, plus
* macOS and native Windows, are untouched they still hit `shell.openPath`.
* If the WSL helpers fail (missing `wslpath`, missing `explorer.exe`,
* non-standard WSL setup), the routing falls back to `shell.openPath`
* rather than surfacing a WSL-specific error.
*/
export interface OpenPathDeps {
release: () => string;
execFile: (command: string, args: readonly string[]) => Promise<{ stdout: string }>;
openPath: (path: string) => Promise<string>;
}
/**
* Returns `""` on success (matching Electron's `shell.openPath` contract so
* the IPC return value is unchanged across platforms), or an error message
* string on failure.
*/
export async function openValidatedDirectory(
resolvedPath: string,
deps: OpenPathDeps,
): Promise<string> {
if (deps.release().toLowerCase().includes("microsoft")) {
let windowsPath: string;
try {
const { stdout } = await deps.execFile("wslpath", ["-w", resolvedPath]);
windowsPath = stdout.trim();
} catch {
return await deps.openPath(resolvedPath);
}
if (windowsPath.length > 0) {
try {
await deps.execFile("explorer.exe", [windowsPath]);
} catch (err) {
// explorer.exe routinely exits non-zero (typically 1) even after
// opening the folder successfully, so a rejected execFile here
// does NOT mean Explorer failed to launch — it just means the
// process exited non-zero. Only fall back to shell.openPath when
// explorer.exe never spawned at all (ENOENT/EACCES); for every
// other error code, treat Explorer as having opened the folder
// and short-circuit the success path. Without this distinction,
// the WSL happy path would still surface the Chrome file://
// listing that #1581 is about, because the post-launch exit-1
// would look identical to a missing binary.
const code =
err && typeof err === "object" && "code" in err
? (err as { code?: unknown }).code
: undefined;
if (code === "ENOENT" || code === "EACCES") {
return await deps.openPath(resolvedPath);
}
}
return "";
}
}
return await deps.openPath(resolvedPath);
}

View file

@ -1,8 +1,11 @@
import { execFile } from "node:child_process";
import { createHmac, randomBytes } from "node:crypto";
import { readFileSync } from "node:fs";
import { appendFile, mkdir, realpath, stat, writeFile } from "node:fs/promises";
import { release } from "node:os";
import { dirname, isAbsolute, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { BrowserWindow, app, dialog, ipcMain, nativeImage, screen, shell } from "electron";
import {
@ -15,10 +18,13 @@ import {
} from "@open-design/sidecar-proto";
import type { OpenDesignHostActionResult, OpenDesignHostUpdaterActionOptions } from "@open-design/host";
import { openValidatedDirectory } from "./open-path.js";
import { createElectronPdfTarget, exportPdfFromHtml, savePrintReadyDocumentAsPdf } from "./pdf-export.js";
import type { PrintReadyPdfOptions } from "./pdf-export.js";
import type { DesktopUpdater } from "./updater.js";
const execFileAsync = promisify(execFile);
/**
* Result of validating a candidate path before exposing it to a
* privileged shell operation.
@ -1204,7 +1210,14 @@ export async function createDesktopRuntime(options: DesktopRuntimeOptions): Prom
const validated = await validateExistingDirectory(resolved.context.resolvedDir);
if (!validated.ok) return `open-path: ${validated.reason}`;
try {
return await shell.openPath(validated.resolved);
return await openValidatedDirectory(validated.resolved, {
release,
execFile: async (cmd, args) => {
const { stdout } = await execFileAsync(cmd, [...args]);
return { stdout };
},
openPath: (p) => shell.openPath(p),
});
} catch (err) {
return err instanceof Error ? err.message : String(err);
}

View file

@ -0,0 +1,173 @@
import { describe, expect, it, vi } from "vitest";
import { openValidatedDirectory, type OpenPathDeps } from "../../src/main/open-path.js";
function makeDeps(overrides: Partial<OpenPathDeps> = {}): OpenPathDeps {
return {
release: () => "5.15.0-100-generic",
execFile: vi.fn(async () => ({ stdout: "" })),
openPath: vi.fn(async () => ""),
...overrides,
};
}
describe("openValidatedDirectory", () => {
describe("non-WSL Linux", () => {
it("hands the path to shell.openPath and never invokes wslpath", async () => {
const execFile = vi.fn(async () => ({ stdout: "" }));
const openPath = vi.fn(async () => "");
const result = await openValidatedDirectory("/home/u/project", makeDeps({
release: () => "5.15.0-100-generic",
execFile,
openPath,
}));
expect(result).toBe("");
expect(openPath).toHaveBeenCalledWith("/home/u/project");
expect(execFile).not.toHaveBeenCalled();
});
it("propagates an error string from shell.openPath", async () => {
const result = await openValidatedDirectory("/home/u/project", makeDeps({
release: () => "5.15.0-100-generic",
openPath: vi.fn(async () => "shell.openPath: permission denied"),
}));
expect(result).toBe("shell.openPath: permission denied");
});
});
describe("WSL", () => {
it("routes through wslpath -w + explorer.exe and returns empty on success", async () => {
const execFile = vi.fn(async (cmd: string, args: readonly string[]) => {
if (cmd === "wslpath" && args[0] === "-w") {
return { stdout: "C:\\Users\\u\\project\n" };
}
if (cmd === "explorer.exe") {
return { stdout: "" };
}
throw new Error(`unexpected execFile call: ${cmd}`);
});
const openPath = vi.fn(async () => "");
const result = await openValidatedDirectory("/mnt/c/Users/u/project", makeDeps({
release: () => "5.15.167.4-microsoft-standard-WSL2",
execFile,
openPath,
}));
expect(result).toBe("");
expect(execFile).toHaveBeenNthCalledWith(1, "wslpath", ["-w", "/mnt/c/Users/u/project"]);
expect(execFile).toHaveBeenNthCalledWith(2, "explorer.exe", ["C:\\Users\\u\\project"]);
expect(openPath).not.toHaveBeenCalled();
});
it("detects WSL release strings regardless of casing", async () => {
const execFile = vi.fn(async (cmd: string, args: readonly string[]) => {
if (cmd === "wslpath") return { stdout: "C:\\Users\\u\\project\n" };
return { stdout: "" };
});
await openValidatedDirectory("/mnt/c/Users/u/project", makeDeps({
release: () => "4.19.128-Microsoft-Standard",
execFile,
}));
expect(execFile).toHaveBeenCalledWith("wslpath", ["-w", "/mnt/c/Users/u/project"]);
});
it("falls back to shell.openPath when wslpath fails", async () => {
const execFile = vi.fn(async (cmd: string) => {
if (cmd === "wslpath") throw new Error("wslpath: command not found");
return { stdout: "" };
});
const openPath = vi.fn(async () => "");
const result = await openValidatedDirectory("/home/u/project", makeDeps({
release: () => "5.15.0-microsoft-standard-WSL2",
execFile,
openPath,
}));
expect(result).toBe("");
expect(openPath).toHaveBeenCalledWith("/home/u/project");
});
it("falls back to shell.openPath when wslpath returns an empty string", async () => {
const execFile = vi.fn(async () => ({ stdout: " \n" }));
const openPath = vi.fn(async () => "");
const result = await openValidatedDirectory("/home/u/project", makeDeps({
release: () => "5.15.0-microsoft-standard-WSL2",
execFile,
openPath,
}));
expect(result).toBe("");
expect(execFile).toHaveBeenCalledTimes(1);
expect(execFile).toHaveBeenCalledWith("wslpath", ["-w", "/home/u/project"]);
expect(openPath).toHaveBeenCalledWith("/home/u/project");
});
it("falls back to shell.openPath when explorer.exe cannot be spawned (ENOENT)", async () => {
const execFile = vi.fn(async (cmd: string) => {
if (cmd === "wslpath") return { stdout: "C:\\Users\\u\\project\n" };
if (cmd === "explorer.exe") {
throw Object.assign(new Error("spawn explorer.exe ENOENT"), { code: "ENOENT" });
}
return { stdout: "" };
});
const openPath = vi.fn(async () => "");
const result = await openValidatedDirectory("/mnt/c/Users/u/project", makeDeps({
release: () => "5.15.0-microsoft-standard-WSL2",
execFile,
openPath,
}));
expect(result).toBe("");
expect(openPath).toHaveBeenCalledWith("/mnt/c/Users/u/project");
});
it("treats a non-zero explorer.exe exit after a successful spawn as success", async () => {
// explorer.exe routinely exits 1 even after opening the folder, so
// a rejected execFile without an ENOENT-style code must not fall
// back to shell.openPath; otherwise the user would see Explorer
// open AND a Chrome file:// tab — the original #1581 symptom.
const execFile = vi.fn(async (cmd: string) => {
if (cmd === "wslpath") return { stdout: "C:\\Users\\u\\project\n" };
if (cmd === "explorer.exe") {
throw Object.assign(new Error("Command failed with exit code 1"), {
code: 1,
stdout: "",
stderr: "",
});
}
return { stdout: "" };
});
const openPath = vi.fn(async () => "");
const result = await openValidatedDirectory("/mnt/c/Users/u/project", makeDeps({
release: () => "5.15.0-microsoft-standard-WSL2",
execFile,
openPath,
}));
expect(result).toBe("");
expect(execFile).toHaveBeenCalledWith("explorer.exe", ["C:\\Users\\u\\project"]);
expect(openPath).not.toHaveBeenCalled();
});
it("falls back to shell.openPath when explorer.exe is blocked (EACCES)", async () => {
const execFile = vi.fn(async (cmd: string) => {
if (cmd === "wslpath") return { stdout: "C:\\Users\\u\\project\n" };
if (cmd === "explorer.exe") {
throw Object.assign(new Error("spawn explorer.exe EACCES"), { code: "EACCES" });
}
return { stdout: "" };
});
const openPath = vi.fn(async () => "");
const result = await openValidatedDirectory("/mnt/c/Users/u/project", makeDeps({
release: () => "5.15.0-microsoft-standard-WSL2",
execFile,
openPath,
}));
expect(result).toBe("");
expect(openPath).toHaveBeenCalledWith("/mnt/c/Users/u/project");
});
});
});