Files
nrwl--nx/e2e/utils/ensure-browser-installation.ts
2026-07-13 12:38:36 +08:00

205 lines
5.2 KiB
TypeScript

import { execSync } from 'node:child_process';
import {
existsSync,
mkdirSync,
readFileSync,
statSync,
unlinkSync,
writeFileSync,
openSync,
closeSync,
} from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { tmpProjPath } from './create-project-utils';
import { e2eConsoleLogger } from './log-utils';
import { isVerbose } from './get-env-info';
// Helper files to prevent multiple `npx playwright install` on the same machine.
// Doing so will cause `apt` to fail on Linux machines.
const LOCK_DIR = join(tmpdir(), 'nx-e2e-locks');
const PLAYWRIGHT_LOCK_FILE = join(LOCK_DIR, 'playwright-install.lock');
const PLAYWRIGHT_STATUS_FILE = join(LOCK_DIR, 'playwright-install.status');
const POLL_INTERVAL_MS = 5000;
const MAX_WAIT_MS = 5 * 60 * 1000;
interface InstallStatus {
status: 'installing' | 'success' | 'failed';
pid: number;
timestamp: number;
error?: string;
}
function ensureLockDir() {
if (!existsSync(LOCK_DIR)) {
mkdirSync(LOCK_DIR, { recursive: true });
}
}
function tryAcquireLock(lockFile: string, statusFile: string): boolean {
ensureLockDir();
try {
// Atomic operation - fails if file exists
const fd = openSync(lockFile, 'wx');
closeSync(fd);
const status: InstallStatus = {
status: 'installing',
pid: process.pid,
timestamp: Date.now(),
};
writeFileSync(statusFile, JSON.stringify(status));
return true;
} catch (err) {
if (err.code === 'EEXIST') {
return false; // Lock already held
}
throw err; // Unexpected error
}
}
function releaseLock(
lockFile: string,
statusFile: string,
success: boolean,
error?: string
) {
try {
const status: InstallStatus = {
status: success ? 'success' : 'failed',
pid: process.pid,
timestamp: Date.now(),
error: error,
};
writeFileSync(statusFile, JSON.stringify(status));
} finally {
try {
unlinkSync(lockFile);
} catch {
// Best effort
}
}
}
function readInstallStatus(statusFile: string): InstallStatus | null {
try {
if (!existsSync(statusFile)) {
return null;
}
const content = readFileSync(statusFile, 'utf-8');
return JSON.parse(content);
} catch {
return null;
}
}
async function waitForInstallation(
lockFile: string,
statusFile: string,
toolName: string
): Promise<void> {
const startTime = Date.now();
while (Date.now() - startTime < MAX_WAIT_MS) {
// Check if lock is released
if (!existsSync(lockFile)) {
const status = readInstallStatus(statusFile);
if (status?.status === 'success') {
e2eConsoleLogger(
`${toolName} browsers installed by process ${status.pid}`
);
return;
}
if (status?.status === 'failed') {
const errorMsg = `${toolName} browser installation failed in process ${
status.pid
}: ${status.error || 'Unknown error'}`;
e2eConsoleLogger(errorMsg);
throw new Error(errorMsg);
}
}
// Wait before polling again
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}
throw new Error(
`Timeout waiting for ${toolName} browser installation to complete`
);
}
export function ensureCypressInstallation() {
let cypressVerified = true;
try {
const r = execSync('npx cypress verify', {
stdio: isVerbose() ? 'inherit' : 'pipe',
encoding: 'utf-8',
cwd: tmpProjPath(),
});
if (r.indexOf('Verified Cypress!') === -1) {
cypressVerified = false;
}
} catch {
cypressVerified = false;
} finally {
if (!cypressVerified) {
e2eConsoleLogger('Cypress was not verified. Installing Cypress now.');
execSync('npx cypress install', {
stdio: isVerbose() ? 'inherit' : 'pipe',
encoding: 'utf-8',
cwd: tmpProjPath(),
});
}
}
}
export async function ensurePlaywrightBrowsersInstallation() {
// If lock is acquired, perform installation, otherwise it must in progress or already done
if (tryAcquireLock(PLAYWRIGHT_LOCK_FILE, PLAYWRIGHT_STATUS_FILE)) {
// We got the lock - we're responsible for installation
e2eConsoleLogger(
`Process ${process.pid} acquired lock, installing Playwright browsers...`
);
const playwrightInstallArgs =
process.env.PLAYWRIGHT_INSTALL_ARGS || '--with-deps';
try {
execSync(`npx playwright install ${playwrightInstallArgs}`, {
stdio: isVerbose() ? 'inherit' : 'pipe',
encoding: 'utf-8',
cwd: tmpProjPath(),
});
const version = execSync('npx playwright --version').toString().trim();
e2eConsoleLogger(
`Playwright browsers ${version} installed successfully.`
);
releaseLock(PLAYWRIGHT_LOCK_FILE, PLAYWRIGHT_STATUS_FILE, true);
} catch (error) {
e2eConsoleLogger('Failed to install Playwright browsers:', error);
releaseLock(
PLAYWRIGHT_LOCK_FILE,
PLAYWRIGHT_STATUS_FILE,
false,
error.message
);
throw error;
}
} else {
e2eConsoleLogger(
`Process ${process.pid} waiting for Playwright browser installation...`
);
await waitForInstallation(
PLAYWRIGHT_LOCK_FILE,
PLAYWRIGHT_STATUS_FILE,
'Playwright'
);
}
}