Files
2026-03-03 23:49:13 +01:00

206 lines
7.4 KiB
JavaScript

/**
* Shared utilities for connecting to the relay server.
* Used by both MCP and CLI.
*/
import { spawn } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import pc from 'picocolors';
import { getListeningPidsForPort, killPortProcess } from './kill-port.js';
import { VERSION, sleep, LOG_FILE_PATH } from './utils.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export const RELAY_PORT = Number(process.env.PLAYWRITER_PORT) || 19988;
export async function getRelayServerVersion(port = RELAY_PORT) {
try {
const response = await fetch(`http://127.0.0.1:${port}/version`, {
signal: AbortSignal.timeout(500),
});
if (!response.ok) {
return null;
}
const data = (await response.json());
return data.version;
}
catch {
return null;
}
}
export async function getExtensionStatus(port = RELAY_PORT) {
try {
const response = await fetch(`http://127.0.0.1:${port}/extension/status`, {
signal: AbortSignal.timeout(500),
});
if (!response.ok) {
return null;
}
return (await response.json());
}
catch {
return null;
}
}
export async function getExtensionsStatus(port = RELAY_PORT) {
try {
const response = await fetch(`http://127.0.0.1:${port}/extensions/status`, {
signal: AbortSignal.timeout(2000),
});
if (!response.ok) {
const fallback = await fetch(`http://127.0.0.1:${port}/extension/status`, {
signal: AbortSignal.timeout(2000),
});
if (!fallback.ok) {
return [];
}
const fallbackData = (await fallback.json());
if (!fallbackData?.connected) {
return [];
}
return [
{
extensionId: 'default',
stableKey: undefined,
browser: fallbackData.browser,
profile: fallbackData.profile,
activeTargets: fallbackData.activeTargets,
playwriterVersion: fallbackData.playwriterVersion || null,
},
];
}
const data = (await response.json());
return data.extensions || [];
}
catch {
return [];
}
}
/**
* Wait for at least one extension to appear in extensions status.
* Returns connected extension entries, or [] on timeout.
*/
export async function waitForConnectedExtensions(options = {}) {
const { port = RELAY_PORT, timeoutMs = 5000, pollIntervalMs = 200, logger } = options;
const startTime = Date.now();
logger?.log(pc.dim('Waiting for extension to connect...'));
while (Date.now() - startTime < timeoutMs) {
const extensions = await getExtensionsStatus(port);
if (extensions.length > 0) {
logger?.log(pc.green('Extension connected'));
return extensions;
}
await sleep(pollIntervalMs);
}
logger?.log(pc.yellow('Extension did not connect within timeout'));
return [];
}
async function killRelayServer(options) {
const { port, waitForFreeMs = 3000 } = options;
try {
await killPortProcess({ port });
}
catch {
return;
}
const startTime = Date.now();
while (Date.now() - startTime < waitForFreeMs) {
const pids = await getListeningPidsForPort({ port }).catch(() => []);
if (pids.length === 0) {
return;
}
await sleep(100);
}
}
/**
* Compare two semver versions. Returns:
* - negative if v1 < v2
* - 0 if v1 === v2
* - positive if v1 > v2
*/
export function compareVersions(v1, v2) {
const parts1 = v1.split('.').map(Number);
const parts2 = v2.split('.').map(Number);
const len = Math.max(parts1.length, parts2.length);
for (let i = 0; i < len; i++) {
const p1 = parts1[i] || 0;
const p2 = parts2[i] || 0;
if (p1 !== p2) {
return p1 - p2;
}
}
return 0;
}
/**
* Check if the running playwriter package is older than the version the extension was built with.
* The extension bundles the playwriter version at build time. If the extension reports a newer
* version, it means the user's CLI/MCP needs updating.
* Returns a warning message if outdated, null otherwise.
*/
export function getExtensionOutdatedWarning(extensionPlaywriterVersion) {
if (!extensionPlaywriterVersion) {
return null;
}
if (compareVersions(extensionPlaywriterVersion, VERSION) > 0) {
return `Playwriter ${VERSION} is outdated (extension requires ${extensionPlaywriterVersion}). Run \`npm install -g playwriter@latest\` or update the playwriter package in your project.`;
}
return null;
}
/**
* Ensures the relay server is running. Starts it if not running.
* Optionally restarts on version mismatch.
*/
export async function ensureRelayServer(options = {}) {
const { logger, restartOnVersionMismatch = true, env: additionalEnv } = options;
const serverVersion = await getRelayServerVersion(RELAY_PORT);
if (serverVersion === VERSION) {
return;
}
// Don't restart if server version is higher than our version.
// This prevents older clients from killing a newer server.
if (serverVersion !== null && compareVersions(serverVersion, VERSION) > 0) {
return;
}
if (serverVersion !== null) {
if (restartOnVersionMismatch) {
logger?.log(pc.yellow(`CDP relay server version mismatch (server: ${serverVersion}, client: ${VERSION}), restarting...`));
await killRelayServer({ port: RELAY_PORT });
}
else {
// Server is running but different version, just use it
return;
}
}
else {
const listeningPids = await getListeningPidsForPort({ port: RELAY_PORT }).catch(() => []);
if (listeningPids.length > 0) {
logger?.log(pc.yellow(`Port ${RELAY_PORT} is already in use (pid(s): ${listeningPids.join(', ')}). Attempting to stop the existing process...`));
await killRelayServer({ port: RELAY_PORT });
}
logger?.log(pc.dim('CDP relay server not running, starting it...'));
}
// Detect if we're running from source (.ts) or compiled (.js)
// This handles: tsx, vite-node, ts-node, or direct node on compiled output
const isRunningFromSource = __filename.endsWith('.ts');
const scriptPath = isRunningFromSource
? path.resolve(__dirname, './start-relay-server.ts')
: path.resolve(__dirname, './start-relay-server.js');
const serverProcess = spawn(isRunningFromSource ? 'tsx' : process.execPath, [scriptPath], {
detached: true,
stdio: 'ignore',
env: { ...process.env, ...additionalEnv },
});
serverProcess.unref();
const startTimeoutMs = 5000;
const startTime = Date.now();
while (Date.now() - startTime < startTimeoutMs) {
await sleep(200);
const newVersion = await getRelayServerVersion(RELAY_PORT);
if (newVersion) {
logger?.log(pc.green('CDP relay server started successfully'));
await sleep(1000);
return true;
}
}
const waitedMs = Date.now() - startTime;
throw new Error(`Failed to start CDP relay server within ${waitedMs}ms. Check logs at: ${LOG_FILE_PATH}`);
}
//# sourceMappingURL=relay-client.js.map