mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
fix(engine): reuse HyperFrames browser cache (#2459)
* fix(engine): reuse HyperFrames browser cache * test(engine): isolate browser cache home on Windows
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import type { Browser, PuppeteerNode } from "puppeteer-core";
|
||||
|
||||
@@ -241,10 +245,13 @@ describe("resolveBrowserGpuMode", () => {
|
||||
|
||||
describe("resolveHeadlessShellPath", () => {
|
||||
const originalHeadlessShellPath = process.env.PRODUCER_HEADLESS_SHELL_PATH;
|
||||
const originalHyperframesBrowserPath = process.env.HYPERFRAMES_BROWSER_PATH;
|
||||
|
||||
afterEach(() => {
|
||||
if (originalHeadlessShellPath === undefined) delete process.env.PRODUCER_HEADLESS_SHELL_PATH;
|
||||
else process.env.PRODUCER_HEADLESS_SHELL_PATH = originalHeadlessShellPath;
|
||||
if (originalHyperframesBrowserPath === undefined) delete process.env.HYPERFRAMES_BROWSER_PATH;
|
||||
else process.env.HYPERFRAMES_BROWSER_PATH = originalHyperframesBrowserPath;
|
||||
});
|
||||
|
||||
it("throws a clear error when PRODUCER_HEADLESS_SHELL_PATH points at a missing binary", () => {
|
||||
@@ -254,6 +261,59 @@ describe("resolveHeadlessShellPath", () => {
|
||||
/Chrome binary not found at PRODUCER_HEADLESS_SHELL_PATH/,
|
||||
);
|
||||
});
|
||||
|
||||
it("uses HYPERFRAMES_BROWSER_PATH when the CLI resolved a browser explicitly", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "hyperframes-engine-browser-env-"));
|
||||
try {
|
||||
const binary = join(dir, "chrome-headless-shell");
|
||||
writeFileSync(binary, "");
|
||||
delete process.env.PRODUCER_HEADLESS_SHELL_PATH;
|
||||
process.env.HYPERFRAMES_BROWSER_PATH = binary;
|
||||
|
||||
expect(resolveHeadlessShellPath({})).toBe(binary);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("reuses chrome-headless-shell from the HyperFrames-managed cache", () => {
|
||||
const home = mkdtempSync(join(tmpdir(), "hyperframes-engine-browser-cache-"));
|
||||
try {
|
||||
const binary = join(
|
||||
home,
|
||||
".cache",
|
||||
"hyperframes",
|
||||
"chrome",
|
||||
"chrome-headless-shell",
|
||||
"linux-152.0.7928.2",
|
||||
"chrome-headless-shell-linux64",
|
||||
"chrome-headless-shell",
|
||||
);
|
||||
mkdirSync(join(binary, ".."), { recursive: true });
|
||||
writeFileSync(binary, "");
|
||||
const olderBinary = binary.replace("linux-152.0.7928.2", "linux-99.0.1.1");
|
||||
mkdirSync(join(olderBinary, ".."), { recursive: true });
|
||||
writeFileSync(olderBinary, "");
|
||||
|
||||
// os.homedir() reads HOME on POSIX and USERPROFILE on Windows.
|
||||
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",
|
||||
`import(${JSON.stringify(moduleUrl)}).then(({ resolveHeadlessShellPath }) => process.stdout.write(resolveHeadlessShellPath({}) ?? ""))`,
|
||||
],
|
||||
{ encoding: "utf8", env },
|
||||
);
|
||||
|
||||
expect(stdout).toBe(binary);
|
||||
} finally {
|
||||
rmSync(home, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("forceReleaseBrowser", () => {
|
||||
|
||||
@@ -112,10 +112,52 @@ async function probeHardwareWebGlInfo(
|
||||
|
||||
export type AcquiredBrowser = BrowserLease;
|
||||
|
||||
function compareBrowserVersionsDescending(left: string, right: string): number {
|
||||
const parse = (value: string): number[] => {
|
||||
const version = value.slice(value.indexOf("-") + 1);
|
||||
const segments: number[] = [];
|
||||
for (const segment of version.split(".")) {
|
||||
const parsed = Number.parseInt(segment, 10);
|
||||
if (!Number.isFinite(parsed)) break;
|
||||
segments.push(parsed);
|
||||
}
|
||||
return segments;
|
||||
};
|
||||
const leftSegments = parse(left);
|
||||
const rightSegments = parse(right);
|
||||
const length = Math.max(leftSegments.length, rightSegments.length);
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
const difference = (rightSegments[index] ?? 0) - (leftSegments[index] ?? 0);
|
||||
if (difference !== 0) return difference;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function findCachedHeadlessShell(baseDir: string): string | undefined {
|
||||
if (!existsSync(baseDir)) return undefined;
|
||||
try {
|
||||
const versions = readdirSync(baseDir).sort(compareBrowserVersionsDescending);
|
||||
for (const version of versions) {
|
||||
const candidates = [
|
||||
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;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore unreadable cache directories and continue browser discovery.
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve chrome-headless-shell binary for deterministic BeginFrame rendering.
|
||||
* Checks config.chromePath, then PRODUCER_HEADLESS_SHELL_PATH env var,
|
||||
* then scans Puppeteer's managed cache at ~/.cache/puppeteer/chrome-headless-shell/.
|
||||
* then the CLI browser override, HyperFrames' managed cache, and Puppeteer's cache.
|
||||
*/
|
||||
export function resolveHeadlessShellPath(
|
||||
config?: Partial<Pick<EngineConfig, "chromePath">>,
|
||||
@@ -133,25 +175,22 @@ export function resolveHeadlessShellPath(
|
||||
}
|
||||
return envPath;
|
||||
}
|
||||
const baseDir = join(homedir(), ".cache", "puppeteer", "chrome-headless-shell");
|
||||
if (!existsSync(baseDir)) return undefined;
|
||||
try {
|
||||
const versions = readdirSync(baseDir).sort().reverse(); // newest first
|
||||
for (const version of versions) {
|
||||
const candidates = [
|
||||
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 (process.env.HYPERFRAMES_BROWSER_PATH) {
|
||||
const envPath = process.env.HYPERFRAMES_BROWSER_PATH;
|
||||
if (!existsSync(envPath)) {
|
||||
throw new Error(
|
||||
`[BrowserManager] Chrome binary not found at HYPERFRAMES_BROWSER_PATH="${envPath}". ` +
|
||||
"Run `hyperframes browser ensure` to re-download.",
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
return envPath;
|
||||
}
|
||||
return undefined;
|
||||
const home = homedir();
|
||||
return (
|
||||
findCachedHeadlessShell(
|
||||
join(home, ".cache", "hyperframes", "chrome", "chrome-headless-shell"),
|
||||
) ?? findCachedHeadlessShell(join(home, ".cache", "puppeteer", "chrome-headless-shell"))
|
||||
);
|
||||
}
|
||||
|
||||
// Preserve the producer-era export so re-export shims keep the same public API.
|
||||
|
||||
Reference in New Issue
Block a user