mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-13 07:40:06 +00:00
fix(cli): select host-compatible cached browser (#2861)
* fix(cli): select host-compatible cached browser * test(engine): make browser cache fixture portable * fix(browser): reject foreign ARM cache binaries
This commit is contained in:
@@ -130,7 +130,9 @@ function installPuppeteerBrowsersMock(
|
|||||||
executablePath: string;
|
executablePath: string;
|
||||||
path?: string;
|
path?: string;
|
||||||
buildId?: string;
|
buildId?: string;
|
||||||
|
platform?: string;
|
||||||
}>;
|
}>;
|
||||||
|
browserPlatform?: string;
|
||||||
installedInHfCacheError?: Error;
|
installedInHfCacheError?: Error;
|
||||||
installResult?: { executablePath: string };
|
installResult?: { executablePath: string };
|
||||||
installImpl?: () => Promise<{ executablePath: string }>;
|
installImpl?: () => Promise<{ executablePath: string }>;
|
||||||
@@ -138,10 +140,15 @@ function installPuppeteerBrowsersMock(
|
|||||||
) {
|
) {
|
||||||
vi.doMock("@puppeteer/browsers", () => ({
|
vi.doMock("@puppeteer/browsers", () => ({
|
||||||
Browser: { CHROMEHEADLESSSHELL: "chrome-headless-shell" },
|
Browser: { CHROMEHEADLESSSHELL: "chrome-headless-shell" },
|
||||||
detectBrowserPlatform: () => "linux",
|
detectBrowserPlatform: () => opts.browserPlatform ?? "linux",
|
||||||
getInstalledBrowsers: opts.installedInHfCacheError
|
getInstalledBrowsers: opts.installedInHfCacheError
|
||||||
? vi.fn().mockRejectedValue(opts.installedInHfCacheError)
|
? vi.fn().mockRejectedValue(opts.installedInHfCacheError)
|
||||||
: vi.fn().mockResolvedValue(opts.installedInHfCache ?? []),
|
: vi.fn().mockResolvedValue(
|
||||||
|
(opts.installedInHfCache ?? []).map((browser) => ({
|
||||||
|
platform: opts.browserPlatform ?? "linux",
|
||||||
|
...browser,
|
||||||
|
})),
|
||||||
|
),
|
||||||
install: vi
|
install: vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockImplementation(
|
.mockImplementation(
|
||||||
@@ -222,6 +229,41 @@ describe("findBrowser — cache resolution", () => {
|
|||||||
expect(result).toEqual({ executablePath: SYSTEM_CHROME, source: "system" });
|
expect(result).toEqual({ executablePath: SYSTEM_CHROME, source: "system" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("ignores a current-version HyperFrames cache entry for another platform", async () => {
|
||||||
|
Object.defineProperty(process, "platform", { value: "darwin", configurable: true });
|
||||||
|
Object.defineProperty(process, "arch", { value: "arm64", configurable: true });
|
||||||
|
const macArm64Binary = join(
|
||||||
|
HF_CACHE,
|
||||||
|
"chrome-headless-shell",
|
||||||
|
"mac_arm-131.0.6778.85",
|
||||||
|
"chrome-headless-shell-mac-arm64",
|
||||||
|
"chrome-headless-shell",
|
||||||
|
);
|
||||||
|
installFsMocks({ existing: new Set([HF_CACHE, HF_BINARY, macArm64Binary]) });
|
||||||
|
installPuppeteerBrowsersMock({
|
||||||
|
browserPlatform: "mac_arm",
|
||||||
|
installedInHfCache: [
|
||||||
|
{
|
||||||
|
browser: "chrome-headless-shell",
|
||||||
|
executablePath: HF_BINARY,
|
||||||
|
buildId: CHROME_VERSION,
|
||||||
|
platform: "linux",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
browser: "chrome-headless-shell",
|
||||||
|
executablePath: macArm64Binary,
|
||||||
|
buildId: CHROME_VERSION,
|
||||||
|
platform: "mac_arm",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { findBrowser } = await import("./manager.js");
|
||||||
|
const result = await findBrowser();
|
||||||
|
|
||||||
|
expect(result).toEqual({ executablePath: macArm64Binary, source: "cache" });
|
||||||
|
});
|
||||||
|
|
||||||
it("re-downloads when the hyperframes cache manifest points at a missing binary", async () => {
|
it("re-downloads when the hyperframes cache manifest points at a missing binary", async () => {
|
||||||
const redownloadedBinary = join(
|
const redownloadedBinary = join(
|
||||||
HF_CACHE,
|
HF_CACHE,
|
||||||
@@ -492,6 +534,91 @@ describe("findBrowser — cache resolution", () => {
|
|||||||
expect(result).toEqual({ executablePath: PUPPETEER_BINARY, source: "cache" });
|
expect(result).toEqual({ executablePath: PUPPETEER_BINARY, source: "cache" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
hostPlatform: "darwin",
|
||||||
|
hostArch: "arm64",
|
||||||
|
expectedDirectory: "chrome-headless-shell-mac-arm64",
|
||||||
|
expectedExecutable: "chrome-headless-shell",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostPlatform: "darwin",
|
||||||
|
hostArch: "x64",
|
||||||
|
expectedDirectory: "chrome-headless-shell-mac-x64",
|
||||||
|
expectedExecutable: "chrome-headless-shell",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostPlatform: "linux",
|
||||||
|
hostArch: "x64",
|
||||||
|
expectedDirectory: "chrome-headless-shell-linux64",
|
||||||
|
expectedExecutable: "chrome-headless-shell",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostPlatform: "win32",
|
||||||
|
hostArch: "ia32",
|
||||||
|
expectedDirectory: "chrome-headless-shell-win32",
|
||||||
|
expectedExecutable: "chrome-headless-shell.exe",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostPlatform: "win32",
|
||||||
|
hostArch: "x64",
|
||||||
|
expectedDirectory: "chrome-headless-shell-win64",
|
||||||
|
expectedExecutable: "chrome-headless-shell.exe",
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
"selects only the host-compatible cached shell on $hostPlatform/$hostArch when every platform is present",
|
||||||
|
async ({ hostPlatform, hostArch, expectedDirectory, expectedExecutable }) => {
|
||||||
|
Object.defineProperty(process, "platform", { value: hostPlatform, configurable: true });
|
||||||
|
Object.defineProperty(process, "arch", { value: hostArch, configurable: true });
|
||||||
|
const version = "host-148.0.7778.97";
|
||||||
|
const candidates = [
|
||||||
|
["chrome-headless-shell-linux64", "chrome-headless-shell"],
|
||||||
|
["chrome-headless-shell-mac-arm64", "chrome-headless-shell"],
|
||||||
|
["chrome-headless-shell-mac-x64", "chrome-headless-shell"],
|
||||||
|
["chrome-headless-shell-win32", "chrome-headless-shell.exe"],
|
||||||
|
["chrome-headless-shell-win64", "chrome-headless-shell.exe"],
|
||||||
|
] as const;
|
||||||
|
const binaries = candidates.map(([directory, executable]) =>
|
||||||
|
join(PUPPETEER_CACHE, version, directory, executable),
|
||||||
|
);
|
||||||
|
const expectedBinary = join(PUPPETEER_CACHE, version, expectedDirectory, expectedExecutable);
|
||||||
|
installFsMocks({
|
||||||
|
existing: new Set([PUPPETEER_CACHE, ...binaries]),
|
||||||
|
dirs: { [PUPPETEER_CACHE]: [version] },
|
||||||
|
});
|
||||||
|
installPuppeteerBrowsersMock();
|
||||||
|
|
||||||
|
const { findBrowser } = await import("./manager.js");
|
||||||
|
const result = await findBrowser();
|
||||||
|
|
||||||
|
expect(result).toEqual({ executablePath: expectedBinary, source: "cache" });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ hostPlatform: "linux", hostArch: "arm64" },
|
||||||
|
{ hostPlatform: "win32", hostArch: "arm64" },
|
||||||
|
])(
|
||||||
|
"does not select a foreign cached shell on unsupported $hostPlatform/$hostArch",
|
||||||
|
async ({ hostPlatform, hostArch }) => {
|
||||||
|
Object.defineProperty(process, "platform", { value: hostPlatform, configurable: true });
|
||||||
|
Object.defineProperty(process, "arch", { value: hostArch, configurable: true });
|
||||||
|
const version = "host-148.0.7778.97";
|
||||||
|
const binaries = [
|
||||||
|
join(PUPPETEER_CACHE, version, "chrome-headless-shell-linux64", "chrome-headless-shell"),
|
||||||
|
join(PUPPETEER_CACHE, version, "chrome-headless-shell-win64", "chrome-headless-shell.exe"),
|
||||||
|
];
|
||||||
|
installFsMocks({
|
||||||
|
existing: new Set([PUPPETEER_CACHE, ...binaries]),
|
||||||
|
dirs: { [PUPPETEER_CACHE]: [version] },
|
||||||
|
});
|
||||||
|
installPuppeteerBrowsersMock();
|
||||||
|
|
||||||
|
const { findBrowser } = await import("./manager.js");
|
||||||
|
await expect(findBrowser()).resolves.toBeUndefined();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("prefers the puppeteer cache over the hyperframes cache when BOTH are populated", async () => {
|
it("prefers the puppeteer cache over the hyperframes cache when BOTH are populated", async () => {
|
||||||
// The HF cache is pinned to `CHROME_VERSION` (131-era) which lags upstream
|
// The HF cache is pinned to `CHROME_VERSION` (131-era) which lags upstream
|
||||||
// by many releases. The engine's `resolveHeadlessShellPath` scans the
|
// by many releases. The engine's `resolveHeadlessShellPath` scans the
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ function findFromEnv(): BrowserResult | undefined {
|
|||||||
*/
|
*/
|
||||||
async function findFromHyperframesCache(): Promise<CacheLookupResult> {
|
async function findFromHyperframesCache(): Promise<CacheLookupResult> {
|
||||||
if (!existsSync(CACHE_DIR)) return {};
|
if (!existsSync(CACHE_DIR)) return {};
|
||||||
const { Browser, getInstalledBrowsers } = await loadPuppeteerBrowsers();
|
const { Browser, detectBrowserPlatform, getInstalledBrowsers } = await loadPuppeteerBrowsers();
|
||||||
// A corrupt cache (stub file where a browser dir is expected, malformed
|
// A corrupt cache (stub file where a browser dir is expected, malformed
|
||||||
// metadata) makes getInstalledBrowsers throw. Treat that as "no cached
|
// metadata) makes getInstalledBrowsers throw. Treat that as "no cached
|
||||||
// browser" so resolution falls through to system/download instead of
|
// browser" so resolution falls through to system/download instead of
|
||||||
@@ -302,9 +302,14 @@ async function findFromHyperframesCache(): Promise<CacheLookupResult> {
|
|||||||
// an older hyperframes version (this pin has moved 131 → 151 → 152 across
|
// an older hyperframes version (this pin has moved 131 → 151 → 152 across
|
||||||
// releases) must NOT satisfy resolution, or an upgrade silently keeps
|
// releases) must NOT satisfy resolution, or an upgrade silently keeps
|
||||||
// running whatever build happened to be cached instead of ever fetching
|
// running whatever build happened to be cached instead of ever fetching
|
||||||
// the version this release actually needs (HF#2060 review).
|
// the version this release actually needs (HF#2060 review). Match platform
|
||||||
|
// as well so a shared/migrated cache cannot return a foreign executable.
|
||||||
|
const hostPlatform = detectBrowserPlatform();
|
||||||
const match = installed.find(
|
const match = installed.find(
|
||||||
(b) => b.browser === Browser.CHROMEHEADLESSSHELL && b.buildId === CHROME_VERSION,
|
(b) =>
|
||||||
|
b.browser === Browser.CHROMEHEADLESSSHELL &&
|
||||||
|
b.buildId === CHROME_VERSION &&
|
||||||
|
b.platform === hostPlatform,
|
||||||
);
|
);
|
||||||
if (match && existsSync(match.executablePath)) {
|
if (match && existsSync(match.executablePath)) {
|
||||||
return { result: { executablePath: match.executablePath, source: "cache" } };
|
return { result: { executablePath: match.executablePath, source: "cache" } };
|
||||||
@@ -383,8 +388,33 @@ function compareVersionDirsDescending(a: string, b: string): number {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CACHED_HEADLESS_SHELL_EXECUTABLES: Readonly<
|
||||||
|
Record<string, readonly [directory: string, executable: string]>
|
||||||
|
> = {
|
||||||
|
"darwin/arm64": ["chrome-headless-shell-mac-arm64", "chrome-headless-shell"],
|
||||||
|
"darwin/x64": ["chrome-headless-shell-mac-x64", "chrome-headless-shell"],
|
||||||
|
"linux/x64": ["chrome-headless-shell-linux64", "chrome-headless-shell"],
|
||||||
|
"win32/ia32": ["chrome-headless-shell-win32", "chrome-headless-shell.exe"],
|
||||||
|
"win32/x64": ["chrome-headless-shell-win64", "chrome-headless-shell.exe"],
|
||||||
|
};
|
||||||
|
|
||||||
|
function cachedHeadlessShellExecutable(
|
||||||
|
hostPlatform = process.platform,
|
||||||
|
hostArch = process.arch,
|
||||||
|
): readonly [directory: string, executable: string] | undefined {
|
||||||
|
// Chrome for Testing cache entries are host-specific. Resolve exactly one
|
||||||
|
// platform/architecture directory so a foreign binary cannot win by probe order.
|
||||||
|
// Chrome for Testing does not publish Linux ARM64 binaries. Windows ARM64 can
|
||||||
|
// emulate x64 only on supported Windows 11 builds, which this platform/arch-only
|
||||||
|
// resolver cannot prove, so both hosts deliberately fall through to system
|
||||||
|
// browser discovery instead of attempting a potentially foreign cached binary.
|
||||||
|
return CACHED_HEADLESS_SHELL_EXECUTABLES[`${hostPlatform}/${hostArch}`];
|
||||||
|
}
|
||||||
|
|
||||||
function findFromPuppeteerCache(): BrowserResult | undefined {
|
function findFromPuppeteerCache(): BrowserResult | undefined {
|
||||||
if (!existsSync(PUPPETEER_CACHE_DIR)) return undefined;
|
if (!existsSync(PUPPETEER_CACHE_DIR)) return undefined;
|
||||||
|
const executable = cachedHeadlessShellExecutable();
|
||||||
|
if (!executable) return undefined;
|
||||||
let versions: string[];
|
let versions: string[];
|
||||||
try {
|
try {
|
||||||
// Numeric semver-style sort, newest first. Lexicographic `.sort().reverse()`
|
// Numeric semver-style sort, newest first. Lexicographic `.sort().reverse()`
|
||||||
@@ -399,28 +429,11 @@ function findFromPuppeteerCache(): BrowserResult | undefined {
|
|||||||
// Same shape as `resolveHeadlessShellPath` in engine/browserManager.ts —
|
// Same shape as `resolveHeadlessShellPath` in engine/browserManager.ts —
|
||||||
// keep them aligned. If puppeteer ever changes the on-disk layout the two
|
// keep them aligned. If puppeteer ever changes the on-disk layout the two
|
||||||
// need to move together.
|
// need to move together.
|
||||||
const candidates = [
|
const binary = join(PUPPETEER_CACHE_DIR, version, ...executable);
|
||||||
join(PUPPETEER_CACHE_DIR, version, "chrome-headless-shell-linux64", "chrome-headless-shell"),
|
|
||||||
join(
|
|
||||||
PUPPETEER_CACHE_DIR,
|
|
||||||
version,
|
|
||||||
"chrome-headless-shell-mac-arm64",
|
|
||||||
"chrome-headless-shell",
|
|
||||||
),
|
|
||||||
join(PUPPETEER_CACHE_DIR, version, "chrome-headless-shell-mac-x64", "chrome-headless-shell"),
|
|
||||||
join(
|
|
||||||
PUPPETEER_CACHE_DIR,
|
|
||||||
version,
|
|
||||||
"chrome-headless-shell-win64",
|
|
||||||
"chrome-headless-shell.exe",
|
|
||||||
),
|
|
||||||
];
|
|
||||||
for (const binary of candidates) {
|
|
||||||
if (existsSync(binary)) {
|
if (existsSync(binary)) {
|
||||||
return { executablePath: binary, source: "cache" };
|
return { executablePath: binary, source: "cache" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -401,6 +401,127 @@ describe("resolveHeadlessShellPath", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
hostPlatform: "darwin",
|
||||||
|
hostArch: "arm64",
|
||||||
|
expectedDirectory: "chrome-headless-shell-mac-arm64",
|
||||||
|
expectedExecutable: "chrome-headless-shell",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostPlatform: "darwin",
|
||||||
|
hostArch: "x64",
|
||||||
|
expectedDirectory: "chrome-headless-shell-mac-x64",
|
||||||
|
expectedExecutable: "chrome-headless-shell",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostPlatform: "linux",
|
||||||
|
hostArch: "x64",
|
||||||
|
expectedDirectory: "chrome-headless-shell-linux64",
|
||||||
|
expectedExecutable: "chrome-headless-shell",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostPlatform: "win32",
|
||||||
|
hostArch: "ia32",
|
||||||
|
expectedDirectory: "chrome-headless-shell-win32",
|
||||||
|
expectedExecutable: "chrome-headless-shell.exe",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostPlatform: "win32",
|
||||||
|
hostArch: "x64",
|
||||||
|
expectedDirectory: "chrome-headless-shell-win64",
|
||||||
|
expectedExecutable: "chrome-headless-shell.exe",
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
"selects only the host-compatible cached shell on $hostPlatform/$hostArch when every platform is present",
|
||||||
|
({ hostPlatform, hostArch, expectedDirectory, expectedExecutable }) => {
|
||||||
|
const home = mkdtempSync(join(tmpdir(), "hyperframes-engine-browser-platform-"));
|
||||||
|
try {
|
||||||
|
const cacheVersion = join(
|
||||||
|
home,
|
||||||
|
".cache",
|
||||||
|
"puppeteer",
|
||||||
|
"chrome-headless-shell",
|
||||||
|
"host-152.0.7928.2",
|
||||||
|
);
|
||||||
|
const candidates = [
|
||||||
|
["chrome-headless-shell-linux64", "chrome-headless-shell"],
|
||||||
|
["chrome-headless-shell-mac-arm64", "chrome-headless-shell"],
|
||||||
|
["chrome-headless-shell-mac-x64", "chrome-headless-shell"],
|
||||||
|
["chrome-headless-shell-win32", "chrome-headless-shell.exe"],
|
||||||
|
["chrome-headless-shell-win64", "chrome-headless-shell.exe"],
|
||||||
|
] as const;
|
||||||
|
for (const [directory, executable] of candidates) {
|
||||||
|
const binary = join(cacheVersion, directory, executable);
|
||||||
|
mkdirSync(join(binary, ".."), { recursive: true });
|
||||||
|
writeFileSync(binary, "");
|
||||||
|
}
|
||||||
|
const expectedBinary = join(cacheVersion, expectedDirectory, expectedExecutable);
|
||||||
|
|
||||||
|
const env = { ...process.env, HOME: home, USERPROFILE: home };
|
||||||
|
delete env.PRODUCER_HEADLESS_SHELL_PATH;
|
||||||
|
delete env.HYPERFRAMES_BROWSER_PATH;
|
||||||
|
const moduleUrl = new URL("./browserManager.ts", import.meta.url).href;
|
||||||
|
const stdout = execFileSync(
|
||||||
|
"bun",
|
||||||
|
[
|
||||||
|
"--eval",
|
||||||
|
`Object.defineProperty(process, "platform", { value: ${JSON.stringify(hostPlatform)} }); Object.defineProperty(process, "arch", { value: ${JSON.stringify(hostArch)} }); import(${JSON.stringify(moduleUrl)}).then(({ resolveHeadlessShellPath }) => process.stdout.write(resolveHeadlessShellPath({}) ?? ""))`,
|
||||||
|
],
|
||||||
|
{ encoding: "utf8", env },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(stdout).toBe(expectedBinary);
|
||||||
|
} finally {
|
||||||
|
rmSync(home, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ hostPlatform: "linux", hostArch: "arm64" },
|
||||||
|
{ hostPlatform: "win32", hostArch: "arm64" },
|
||||||
|
])(
|
||||||
|
"does not select a foreign cached shell on unsupported $hostPlatform/$hostArch",
|
||||||
|
({ hostPlatform, hostArch }) => {
|
||||||
|
const home = mkdtempSync(join(tmpdir(), "hyperframes-engine-browser-unsupported-"));
|
||||||
|
try {
|
||||||
|
const cacheVersion = join(
|
||||||
|
home,
|
||||||
|
".cache",
|
||||||
|
"puppeteer",
|
||||||
|
"chrome-headless-shell",
|
||||||
|
"host-152.0.7928.2",
|
||||||
|
);
|
||||||
|
for (const [directory, executable] of [
|
||||||
|
["chrome-headless-shell-linux64", "chrome-headless-shell"],
|
||||||
|
["chrome-headless-shell-win64", "chrome-headless-shell.exe"],
|
||||||
|
] as const) {
|
||||||
|
const binary = join(cacheVersion, directory, executable);
|
||||||
|
mkdirSync(join(binary, ".."), { recursive: true });
|
||||||
|
writeFileSync(binary, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
const env = { ...process.env, HOME: home, USERPROFILE: home };
|
||||||
|
delete env.PRODUCER_HEADLESS_SHELL_PATH;
|
||||||
|
delete env.HYPERFRAMES_BROWSER_PATH;
|
||||||
|
const moduleUrl = new URL("./browserManager.ts", import.meta.url).href;
|
||||||
|
const stdout = execFileSync(
|
||||||
|
"bun",
|
||||||
|
[
|
||||||
|
"--eval",
|
||||||
|
`Object.defineProperty(process, "platform", { value: ${JSON.stringify(hostPlatform)} }); Object.defineProperty(process, "arch", { value: ${JSON.stringify(hostArch)} }); import(${JSON.stringify(moduleUrl)}).then(({ resolveHeadlessShellPath }) => process.stdout.write(resolveHeadlessShellPath({}) ?? ""))`,
|
||||||
|
],
|
||||||
|
{ encoding: "utf8", env },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(stdout).toBe("");
|
||||||
|
} finally {
|
||||||
|
rmSync(home, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("reuses chrome-headless-shell from the HyperFrames-managed cache", () => {
|
it("reuses chrome-headless-shell from the HyperFrames-managed cache", () => {
|
||||||
const home = mkdtempSync(join(tmpdir(), "hyperframes-engine-browser-cache-"));
|
const home = mkdtempSync(join(tmpdir(), "hyperframes-engine-browser-cache-"));
|
||||||
try {
|
try {
|
||||||
@@ -429,7 +550,7 @@ describe("resolveHeadlessShellPath", () => {
|
|||||||
"bun",
|
"bun",
|
||||||
[
|
[
|
||||||
"--eval",
|
"--eval",
|
||||||
`import(${JSON.stringify(moduleUrl)}).then(({ resolveHeadlessShellPath }) => process.stdout.write(resolveHeadlessShellPath({}) ?? ""))`,
|
`Object.defineProperty(process, "platform", { value: "linux" }); Object.defineProperty(process, "arch", { value: "x64" }); import(${JSON.stringify(moduleUrl)}).then(({ resolveHeadlessShellPath }) => process.stdout.write(resolveHeadlessShellPath({}) ?? ""))`,
|
||||||
],
|
],
|
||||||
{ encoding: "utf8", env },
|
{ encoding: "utf8", env },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -133,21 +133,39 @@ function compareBrowserVersionsDescending(left: string, right: string): number {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CACHED_HEADLESS_SHELL_EXECUTABLES: Readonly<
|
||||||
|
Record<string, readonly [directory: string, executable: string]>
|
||||||
|
> = {
|
||||||
|
"darwin/arm64": ["chrome-headless-shell-mac-arm64", "chrome-headless-shell"],
|
||||||
|
"darwin/x64": ["chrome-headless-shell-mac-x64", "chrome-headless-shell"],
|
||||||
|
"linux/x64": ["chrome-headless-shell-linux64", "chrome-headless-shell"],
|
||||||
|
"win32/ia32": ["chrome-headless-shell-win32", "chrome-headless-shell.exe"],
|
||||||
|
"win32/x64": ["chrome-headless-shell-win64", "chrome-headless-shell.exe"],
|
||||||
|
};
|
||||||
|
|
||||||
|
function cachedHeadlessShellExecutable(
|
||||||
|
hostPlatform = process.platform,
|
||||||
|
hostArch = process.arch,
|
||||||
|
): readonly [directory: string, executable: string] | undefined {
|
||||||
|
// Chrome for Testing cache entries are host-specific. Resolve exactly one
|
||||||
|
// platform/architecture directory so a foreign binary cannot win by probe order.
|
||||||
|
// Chrome for Testing does not publish Linux ARM64 binaries. Windows ARM64 can
|
||||||
|
// emulate x64 only on supported Windows 11 builds, which this platform/arch-only
|
||||||
|
// resolver cannot prove, so both hosts deliberately fall through to system
|
||||||
|
// browser discovery instead of attempting a potentially foreign cached binary.
|
||||||
|
return CACHED_HEADLESS_SHELL_EXECUTABLES[`${hostPlatform}/${hostArch}`];
|
||||||
|
}
|
||||||
|
|
||||||
function findCachedHeadlessShell(baseDir: string): string | undefined {
|
function findCachedHeadlessShell(baseDir: string): string | undefined {
|
||||||
if (!existsSync(baseDir)) return undefined;
|
if (!existsSync(baseDir)) return undefined;
|
||||||
|
const executable = cachedHeadlessShellExecutable();
|
||||||
|
if (!executable) return undefined;
|
||||||
try {
|
try {
|
||||||
const versions = readdirSync(baseDir).sort(compareBrowserVersionsDescending);
|
const versions = readdirSync(baseDir).sort(compareBrowserVersionsDescending);
|
||||||
for (const version of versions) {
|
for (const version of versions) {
|
||||||
const candidates = [
|
const binary = join(baseDir, version, ...executable);
|
||||||
join(baseDir, version, "chrome-headless-shell-linux64", "chrome-headless-shell"),
|
|
||||||
join(baseDir, version, "chrome-headless-shell-mac-arm64", "chrome-headless-shell"),
|
|
||||||
join(baseDir, version, "chrome-headless-shell-mac-x64", "chrome-headless-shell"),
|
|
||||||
join(baseDir, version, "chrome-headless-shell-win64", "chrome-headless-shell.exe"),
|
|
||||||
];
|
|
||||||
for (const binary of candidates) {
|
|
||||||
if (existsSync(binary)) return binary;
|
if (existsSync(binary)) return binary;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore unreadable cache directories and continue browser discovery.
|
// Ignore unreadable cache directories and continue browser discovery.
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user