Files
hyperframes/packages/gcp-cloud-run/src/chromium.ts
T

97 lines
3.6 KiB
TypeScript

/**
* Cloud Run Chrome resolver.
*
* `renderChunk()` (the only primitive that needs a browser) launches Chrome
* via the engine's `BrowserManager`. Because Cloud Run runs a container
* image rather than a size-capped ZIP, the Chrome story is far simpler than
* the Lambda adapter's: the `Dockerfile` installs `chrome-headless-shell`
* into the image at a known path and exports `HYPERFRAMES_CHROME_PATH`.
* The Dockerfile proves that exact executable's full BeginFrame screenshot
* contract during the image build. There is no runtime decompression-into-
* /tmp step and no 250 MB packaging ceiling to fight.
*
* Resolution order:
* 1. `PRODUCER_HEADLESS_SHELL_PATH` — the engine's own override. If a
* caller (or the Docker image) already set it, honour it untouched.
* 2. `HYPERFRAMES_CHROME_PATH` — set by the Dockerfile to the installed
* `chrome-headless-shell` binary.
* 3. A small list of conventional install paths, as a last resort for
* images built outside our Dockerfile.
*
* Throws {@link ChromeBinaryUnavailableError} when nothing resolves, so a
* misconfigured image fails loudly at the first chunk rather than emitting
* a confusing puppeteer-core "executablePath must be specified" assertion.
*/
import { existsSync } from "node:fs";
/**
* Thrown when the Chrome binary resolver can't produce a usable path. The
* class name is the workflow's non-retryable error discriminator.
*/
export class ChromeBinaryUnavailableError extends Error {
// Read indirectly via the error envelope / Error.prototype.toString.
// fallow-ignore-next-line unused-class-member
override readonly name = "ChromeBinaryUnavailableError";
readonly resolvedPath: string | null;
constructor(resolvedPath: string | null, hint: string) {
super(`[chromium] Chrome binary unavailable: ${hint}`);
this.resolvedPath = resolvedPath;
}
}
/**
* Conventional locations a `chrome-headless-shell` (or full Chrome) binary
* may live at in a Debian/Ubuntu-based container. Checked only after the
* two env-var overrides miss.
*/
const FALLBACK_CHROME_PATHS = [
"/opt/chrome/chrome-headless-shell",
"/usr/bin/chrome-headless-shell",
"/usr/bin/google-chrome",
"/usr/bin/google-chrome-stable",
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
];
/**
* Resolve the absolute path to a Chrome binary suitable for BeginFrame.
* Pure (no env mutation) so callers decide whether to export the result
* into `PRODUCER_HEADLESS_SHELL_PATH`.
*/
// fallow-ignore-next-line complexity
export function resolveChromeExecutablePath(): string {
const fromEngineOverride = process.env.PRODUCER_HEADLESS_SHELL_PATH?.trim();
if (fromEngineOverride) {
if (!existsSync(fromEngineOverride)) {
throw new ChromeBinaryUnavailableError(
fromEngineOverride,
`PRODUCER_HEADLESS_SHELL_PATH=${JSON.stringify(fromEngineOverride)} does not exist on disk.`,
);
}
return fromEngineOverride;
}
const fromImage = process.env.HYPERFRAMES_CHROME_PATH?.trim();
if (fromImage) {
if (!existsSync(fromImage)) {
throw new ChromeBinaryUnavailableError(
fromImage,
`HYPERFRAMES_CHROME_PATH=${JSON.stringify(fromImage)} does not exist on disk.`,
);
}
return fromImage;
}
for (const candidate of FALLBACK_CHROME_PATHS) {
if (existsSync(candidate)) return candidate;
}
throw new ChromeBinaryUnavailableError(
null,
"no Chrome binary found. Set HYPERFRAMES_CHROME_PATH (the Dockerfile does this) or " +
"PRODUCER_HEADLESS_SHELL_PATH to the absolute path of a chrome-headless-shell binary. " +
`Searched: ${FALLBACK_CHROME_PATHS.join(", ")}.`,
);
}