restart ws process if already running with different version

This commit is contained in:
Tommy D. Rossi
2025-12-21 16:48:07 +01:00
parent ba8096fd74
commit 6809ebea72
9 changed files with 239 additions and 117 deletions
+1
View File
@@ -45,6 +45,7 @@
"devtools-protocol": "^0.0.1543509",
"diff": "^8.0.2",
"hono": "^4.10.6",
"kill-port-process": "^3.2.1",
"playwright-core": "^1.56.1",
"string-dedent": "^3.0.2",
"user-agents": "^1.1.669",
-21
View File
@@ -9,33 +9,12 @@ export type Logger = {
logFilePath: string
}
function cleanupOldLogs(logsDir: string): void {
if (!fs.existsSync(logsDir)) {
return
}
const files = fs.readdirSync(logsDir)
.filter((f) => f.startsWith('relay-server-') && f.endsWith('.log'))
.map((f) => ({
name: f,
path: path.join(logsDir, f),
mtime: fs.statSync(path.join(logsDir, f)).mtime.getTime(),
}))
.sort((a, b) => b.mtime - a.mtime)
if (files.length > 10) {
files.slice(10).forEach((f) => {
fs.unlinkSync(f.path)
})
}
}
export function createFileLogger({ logFilePath }: { logFilePath?: string } = {}): Logger {
const resolvedLogFilePath = logFilePath || LOG_FILE_PATH
const logDir = path.dirname(resolvedLogFilePath)
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true })
}
cleanupOldLogs(logDir)
fs.writeFileSync(resolvedLogFilePath, '')
let queue: Promise<void> = Promise.resolve()
+5
View File
@@ -7,6 +7,7 @@ import type { CDPCommand, CDPResponseBase, CDPEventBase, CDPEventFor, RelayServe
import type { ExtensionMessage, ExtensionEventMessage } from './protocol.js'
import chalk from 'chalk'
import { EventEmitter } from 'node:events'
import { VERSION } from '../utils.js'
type ConnectedTarget = {
sessionId: string
@@ -276,6 +277,10 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
return c.text('OK')
})
app.get('/version', (c) => {
return c.json({ version: VERSION })
})
app.post('/mcp-log', async (c) => {
try {
const { level, args } = await c.req.json()
+14 -12
View File
@@ -14,6 +14,7 @@ import { getCDPSessionForPage } from './cdp-session.js'
import { startPlayWriterCDPRelayServer, type RelayServer } from './extension/cdp-relay.js'
import { createFileLogger } from './create-logger.js'
import type { CDPCommand } from './cdp-types.js'
import { killPortProcess } from 'kill-port-process'
declare const window: any
declare const document: any
@@ -51,15 +52,10 @@ function js(strings: TemplateStringsArray, ...values: any[]): string {
async function killProcessOnPort(port: number): Promise<void> {
try {
const { stdout } = await execAsync(`lsof -ti:${port}`)
const pids = stdout.trim().split('\n').filter(Boolean)
if (pids.length > 0) {
await execAsync(`kill -9 ${pids.join(' ')}`)
console.log(`Killed processes ${pids.join(', ')} on port ${port}`)
await new Promise((resolve) => setTimeout(resolve, 1000))
}
} catch (error) {
// No process running on port or already killed
await killPortProcess(port)
console.log(`Killed processes on port ${port}`)
} catch (err) {
console.error('Error killing process on port:', err)
}
}
@@ -1635,7 +1631,7 @@ describe('MCP Server Tests', () => {
const wsUrl = getCdpUrl()
const client = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
const layoutMetrics = await client.send('Page.getLayoutMetrics')
expect(layoutMetrics.cssVisualViewport).toBeDefined()
expect(layoutMetrics.cssVisualViewport.clientWidth).toBeGreaterThan(0)
@@ -1968,14 +1964,20 @@ describe('CDP Session Tests', () => {
sampleFunctionNames: functionNames,
}).toMatchInlineSnapshot(`
{
"durationMicroseconds": 15772,
"durationMicroseconds": 7398,
"hasNodes": true,
"nodeCount": 7,
"nodeCount": 21,
"sampleFunctionNames": [
"(root)",
"(program)",
"(idle)",
"evaluate",
"fibonacci",
"fibonacci",
"fibonacci",
"fibonacci",
"fibonacci",
"fibonacci",
],
}
`)
+32 -17
View File
@@ -11,7 +11,8 @@ import { fileURLToPath } from 'node:url'
import vm from 'node:vm'
import dedent from 'string-dedent'
import { createPatch } from 'diff'
import { getCdpUrl, LOG_FILE_PATH } from './utils.js'
import { getCdpUrl, LOG_FILE_PATH, VERSION } from './utils.js'
import { killPortProcess } from 'kill-port-process'
import { waitForPageLoad, WaitForPageLoadOptions, WaitForPageLoadResult } from './wait-for-page-load.js'
import { getCDPSessionForPage, CDPSession } from './cdp-session.js'
@@ -152,7 +153,7 @@ function clearConnectionState() {
async function sendLogToRelayServer(level: string, ...args: any[]) {
try {
await fetch(`http://localhost:${RELAY_PORT}/mcp-log`, {
await fetch(`http://127.0.0.1:${RELAY_PORT}/mcp-log`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ level, args }),
@@ -163,25 +164,41 @@ async function sendLogToRelayServer(level: string, ...args: any[]) {
}
}
async function isPortTaken(port: number): Promise<boolean> {
async function getServerVersion(port: number): Promise<string | null> {
try {
const response = await fetch(`http://localhost:${port}/`, {
signal: AbortSignal.timeout(100),
const response = await fetch(`http://127.0.0.1:${port}/version`, {
signal: AbortSignal.timeout(500),
})
return response.ok
} catch {
return false
if (!response.ok) {
return null
}
const data = (await response.json()) as { version: string }
return data.version
} catch (error) {
return null
}
}
async function ensureRelayServer(): Promise<void> {
const portTaken = await isPortTaken(RELAY_PORT)
async function killRelayServer(port: number): Promise<void> {
try {
await killPortProcess(port)
await new Promise((resolve) => setTimeout(resolve, 500))
} catch {}
}
if (portTaken) {
async function ensureRelayServer(): Promise<void> {
const serverVersion = await getServerVersion(RELAY_PORT)
if (serverVersion === VERSION) {
return
}
console.error('CDP relay server not running, starting it...')
if (serverVersion !== null) {
console.error(`CDP relay server version mismatch (server: ${serverVersion}, mcp: ${VERSION}), restarting...`)
await killRelayServer(RELAY_PORT)
} else {
console.error('CDP relay server not running, starting it...')
}
const scriptPath = require.resolve('../dist/start-relay-server.js')
@@ -194,8 +211,8 @@ async function ensureRelayServer(): Promise<void> {
for (let i = 0; i < 10; i++) {
await new Promise((resolve) => setTimeout(resolve, 500))
const isRunning = await isPortTaken(RELAY_PORT)
if (isRunning) {
const newVersion = await getServerVersion(RELAY_PORT)
if (newVersion === VERSION) {
console.error('CDP relay server started successfully')
return
}
@@ -622,9 +639,7 @@ server.tool(
timeout,
displayErrors: true,
}),
new Promise((_, reject) =>
setTimeout(() => reject(new CodeExecutionTimeoutError(timeout)), timeout),
),
new Promise((_, reject) => setTimeout(() => reject(new CodeExecutionTimeoutError(timeout)), timeout)),
])
let responseText = formatConsoleLogs(consoleLogs)
@@ -69,76 +69,76 @@ Return value:
- generic [ref=e50]: Theme
- combobox "Theme" [ref=e51]:
- generic [ref=e52]: "Theme:"
- generic: Neutral
- img
- button "Copy Code" [ref=e53]:
- combobox [ref=e53]
- button "Copy Code" [ref=e54]:
- img
- generic [ref=e54]: Copy Code
- generic [ref=e58]:
- generic [ref=e62]:
- group "Payment Method" [ref=e63]:
- generic [ref=e64]: Payment Method
- paragraph [ref=e65]: All transactions are secure and encrypted
- generic [ref=e66]:
- group [ref=e67]:
- generic [ref=e68]: Name on Card
- textbox "Name on Card" [ref=e69]:
- generic [ref=e55]: Copy Code
- generic [ref=e59]:
- generic [ref=e63]:
- group "Payment Method" [ref=e64]:
- generic [ref=e65]: Payment Method
- paragraph [ref=e66]: All transactions are secure and encrypted
- generic [ref=e67]:
- group [ref=e68]:
- generic [ref=e69]: Name on Card
- textbox "Name on Card" [ref=e70]:
- /placeholder: John Doe
- generic [ref=e70]:
- group [ref=e71]:
- generic [ref=e72]: Card Number
- textbox "Card Number" [ref=e73]:
- generic [ref=e71]:
- group [ref=e72]:
- generic [ref=e73]: Card Number
- textbox "Card Number" [ref=e74]:
- /placeholder: 1234 5678 9012 3456
- paragraph [ref=e74]: Enter your 16-digit number.
- group [ref=e75]:
- generic [ref=e76]: CVV
- textbox "CVV" [ref=e77]:
- paragraph [ref=e75]: Enter your 16-digit number.
- group [ref=e76]:
- generic [ref=e77]: CVV
- textbox "CVV" [ref=e78]:
- /placeholder: "123"
- generic [ref=e78]:
- group [ref=e79]:
- generic [ref=e80]: Month
- combobox "Month" [ref=e81]:
- generic [ref=e79]:
- group [ref=e80]:
- generic [ref=e81]: Month
- combobox "Month" [ref=e82]:
- generic: MM
- img
- combobox [ref=e82]
- group [ref=e83]:
- generic [ref=e84]: Year
- combobox "Year" [ref=e85]:
- combobox [ref=e83]
- group [ref=e84]:
- generic [ref=e85]: Year
- combobox "Year" [ref=e86]:
- generic: YYYY
- img
- combobox [ref=e86]
- group "Billing Address" [ref=e88]:
- generic [ref=e89]: Billing Address
- paragraph [ref=e90]: The billing address associated with your payment method
- group [ref=e92]:
- checkbox "Same as shipping address" [checked] [ref=e93]:
- combobox [ref=e87]
- group "Billing Address" [ref=e89]:
- generic [ref=e90]: Billing Address
- paragraph [ref=e91]: The billing address associated with your payment method
- group [ref=e93]:
- checkbox "Same as shipping address" [checked] [ref=e94]:
- generic:
- img
- checkbox [checked]
- generic [ref=e94]: Same as shipping address
- group [ref=e96]:
- group [ref=e98]:
- generic [ref=e99]: Comments
- textbox "Comments" [ref=e100]:
- generic [ref=e95]: Same as shipping address
- group [ref=e97]:
- group [ref=e99]:
- generic [ref=e100]: Comments
- textbox "Comments" [ref=e101]:
- /placeholder: Add any additional comments
- group [ref=e101]:
- button "Submit" [ref=e102]
- button "Cancel" [ref=e103]
- generic [ref=e104]:
- generic [ref=e105]:
- generic [ref=e106]:
- generic [ref=e108]:
- img "@shadcn" [ref=e110]
- img "@maxleiter" [ref=e112]
- img "@evilrabbit" [ref=e114]
- generic [ref=e115]: No Team Members
- generic [ref=e116]: Invite your team to collaborate on this project.
- button "Invite Members" [ref=e118]:
- group [ref=e102]:
- button "Submit" [ref=e103]
- button "Cancel" [ref=e104]
- generic [ref=e105]:
- generic [ref=e106]:
- generic [ref=e107]:
- generic [ref=e109]:
- generic [ref=e111]: CN
- generic [ref=e113]: LR
- generic [ref=e115]: ER
- generic [ref=e116]: No Team Members
- generic [ref=e117]: Invite your team to collaborate on this project.
- button "Invite Members" [ref=e119]:
- img
- text: Invite Members
- generic [ref=e119]:
- generic [ref=e120]:
- generic [ref=e120]:
- generic [ref=e121]:
- status "Loading"
- tex
[Truncated to 6000 characters. Better manage your logs or paginate them to read the full logs]
+4 -12
View File
@@ -1,22 +1,14 @@
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
export function getCdpUrl({ port = 19988, host = '127.0.0.1' }: { port?: number; host?: string } = {}) {
const id = `${Math.random().toString(36).substring(2, 15)}_${Date.now()}`
return `ws://${host}:${port}/cdp/${id}`
}
function getLogFilePath(): string {
if (process.env.PLAYWRITER_LOG_PATH) {
return process.env.PLAYWRITER_LOG_PATH
}
const logsDir = path.join(os.tmpdir(), 'playwriter')
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true })
}
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
return path.join(logsDir, `relay-server-${timestamp}.log`)
}
export const LOG_FILE_PATH = path.join(os.tmpdir(), 'playwriter', 'relay-server.log')
export const LOG_FILE_PATH = getLogFilePath()
const packageJsonPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json')
export const VERSION = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')).version as string