206 lines
7.4 KiB
JavaScript
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
|