fix(coding-agent): validate Linux browser executables
This commit is contained in:
@@ -133,7 +133,7 @@ let chromiumExecutablePromise: Promise<string | undefined> | undefined;
|
|||||||
export async function ensureChromiumExecutable(): Promise<string | undefined> {
|
export async function ensureChromiumExecutable(): Promise<string | undefined> {
|
||||||
const envPath = process.env.PUPPETEER_EXECUTABLE_PATH;
|
const envPath = process.env.PUPPETEER_EXECUTABLE_PATH;
|
||||||
if (envPath) return envPath;
|
if (envPath) return envPath;
|
||||||
const sysChrome = resolveSystemChromium();
|
const sysChrome = await resolveSystemChromium();
|
||||||
if (sysChrome) return sysChrome;
|
if (sysChrome) return sysChrome;
|
||||||
if (chromiumExecutablePromise) return chromiumExecutablePromise;
|
if (chromiumExecutablePromise) return chromiumExecutablePromise;
|
||||||
|
|
||||||
@@ -193,7 +193,25 @@ let resolvedChromium: string | null | undefined; // undefined = unchecked; null
|
|||||||
function isExecutableFile(p: string): boolean {
|
function isExecutableFile(p: string): boolean {
|
||||||
try {
|
try {
|
||||||
const st = fs.statSync(p);
|
const st = fs.statSync(p);
|
||||||
return st.isFile();
|
if (!st.isFile()) return false;
|
||||||
|
if (process.platform === "win32") return true;
|
||||||
|
fs.accessSync(p, fs.constants.X_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isChromiumExecutable(p: string): Promise<boolean> {
|
||||||
|
if (!isExecutableFile(p)) return false;
|
||||||
|
try {
|
||||||
|
const proc = Bun.spawn([p, "--version"], {
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "ignore",
|
||||||
|
});
|
||||||
|
const stdout = await new Response(proc.stdout).text();
|
||||||
|
await proc.exited;
|
||||||
|
return proc.exitCode === 0 && /Chrom|Edg/i.test(stdout);
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -278,13 +296,13 @@ function systemChromiumCandidates(
|
|||||||
return candidates;
|
return candidates;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveSystemChromium(): string | undefined {
|
async function resolveSystemChromium(): Promise<string | undefined> {
|
||||||
if (resolvedChromium !== undefined) return resolvedChromium ?? undefined;
|
if (resolvedChromium !== undefined) return resolvedChromium ?? undefined;
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
for (const candidate of systemChromiumCandidates()) {
|
for (const candidate of systemChromiumCandidates()) {
|
||||||
if (!candidate || seen.has(candidate)) continue;
|
if (!candidate || seen.has(candidate)) continue;
|
||||||
seen.add(candidate);
|
seen.add(candidate);
|
||||||
if (isExecutableFile(candidate)) {
|
if (await isChromiumExecutable(candidate)) {
|
||||||
resolvedChromium = candidate;
|
resolvedChromium = candidate;
|
||||||
logger.debug("Using system Chrome/Chromium", { path: candidate });
|
logger.debug("Using system Chrome/Chromium", { path: candidate });
|
||||||
return candidate;
|
return candidate;
|
||||||
@@ -894,6 +912,10 @@ export function systemChromiumCandidatesForTest(
|
|||||||
return systemChromiumCandidates(platform, home, which);
|
return systemChromiumCandidates(platform, home, which);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function chromiumExecutableProbeForTest(executablePath: string): Promise<boolean> {
|
||||||
|
return isChromiumExecutable(executablePath);
|
||||||
|
}
|
||||||
|
|
||||||
export function stealthIgnoreDefaultArgsForTest(executablePath: string | undefined): string[] {
|
export function stealthIgnoreDefaultArgsForTest(executablePath: string | undefined): string[] {
|
||||||
return stealthIgnoreDefaultArgs(executablePath);
|
return stealthIgnoreDefaultArgs(executablePath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { describe, expect, it } from "bun:test";
|
import { describe, expect, it } from "bun:test";
|
||||||
|
|
||||||
|
import * as fs from "node:fs";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import {
|
import {
|
||||||
|
chromiumExecutableProbeForTest,
|
||||||
stealthIgnoreDefaultArgsForTest,
|
stealthIgnoreDefaultArgsForTest,
|
||||||
systemChromiumCandidatesForTest,
|
systemChromiumCandidatesForTest,
|
||||||
} from "@oh-my-pi/pi-coding-agent/tools/browser/launch";
|
} from "@oh-my-pi/pi-coding-agent/tools/browser/launch";
|
||||||
@@ -97,6 +99,26 @@ describe("system Chromium candidates", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("browser executable selection", () => {
|
describe("browser executable selection", () => {
|
||||||
|
it.skipIf(process.platform === "win32")(
|
||||||
|
"rejects executable wrappers that are not Chromium-family browsers",
|
||||||
|
async () => {
|
||||||
|
const tempDir = TempDir.createSync("@browser-probe-");
|
||||||
|
try {
|
||||||
|
const wrapper = path.join(tempDir.path(), "google-chrome");
|
||||||
|
const chromium = path.join(tempDir.path(), "chromium");
|
||||||
|
await Bun.write(wrapper, "#!/bin/sh\necho browser bridge\n");
|
||||||
|
await Bun.write(chromium, "#!/bin/sh\necho Chromium 123.0\n");
|
||||||
|
fs.chmodSync(wrapper, 0o755);
|
||||||
|
fs.chmodSync(chromium, 0o755);
|
||||||
|
|
||||||
|
await expect(chromiumExecutableProbeForTest(wrapper)).resolves.toBe(false);
|
||||||
|
await expect(chromiumExecutableProbeForTest(chromium)).resolves.toBe(true);
|
||||||
|
} finally {
|
||||||
|
await tempDir.remove();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("honors PUPPETEER_EXECUTABLE_PATH before a detected Windows system Chrome", async () => {
|
it("honors PUPPETEER_EXECUTABLE_PATH before a detected Windows system Chrome", async () => {
|
||||||
const tempDir = TempDir.createSync("@browser-executable-");
|
const tempDir = TempDir.createSync("@browser-executable-");
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user