Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 37 additions & 10 deletions packages/playwright-core/src/tools/dashboard/dashboardApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import http from 'http';
import { HttpServer } from '@utils/httpServer';
import { makeSocketPath } from '@utils/fileUtils';
import { gracefullyProcessExitDoNotHang } from '@utils/processLauncher';
import { monotonicTime } from '@isomorphic/time';
import { libPath } from '../../package';
import { playwright } from '../../inprocess';
import { findChromiumChannelBestEffort, registryDirectory } from '../../server/registry/index';
Expand Down Expand Up @@ -280,9 +281,7 @@ async function acquireSingleton(options: DashboardOptions): Promise<net.Server>
const server = net.createServer();
server.listen(socketPath, () => resolve(server));
server.on('error', (err: NodeJS.ErrnoException) => {
const isInUse = err.code === 'EADDRINUSE'
|| (process.platform === 'win32' && err.code === 'EBUSY');
if (!isInUse)
if (err.code !== 'EADDRINUSE')
return reject(err);
const client = net.connect(socketPath, () => {
client.write(JSON.stringify(options) + '\n');
Expand Down Expand Up @@ -345,16 +344,21 @@ export async function openDashboardApp() {
socket.end();
return;
}
if (parsed.kill) {
// Write our PID so the kill client can wait for the process to fully exit,
// which guarantees the named pipe is released (especially on Windows).
// Start graceful shutdown only after the socket data has been flushed, so the
// kill client is guaranteed to receive the PID before we begin tearing down.
server?.close();
socket.end(JSON.stringify({ pid: process.pid }) + '\n', () => gracefullyProcessExitDoNotHang(0));
return;
}
void statePromise.then(({ page, server: dashboard }) => {
if (parsed.annotate) {
page?.bringToFront().catch(() => {});
dashboard.reveal(parsed);
dashboard.triggerAnnotate();
dashboard.registerAnnotateWaiter(socket);
} else if (parsed.kill) {
server?.close();
socket.end();
gracefullyProcessExitDoNotHang(0);
} else {
page?.bringToFront().catch(() => {});
dashboard.reveal(parsed);
Expand Down Expand Up @@ -384,14 +388,37 @@ export async function openDashboardForContext(context: api.BrowserContext): Prom

async function runKillClient(): Promise<void> {
const socketPath = dashboardSocketPath();
await new Promise<void>(resolve => {
const pid = await new Promise<number | undefined>((resolve, reject) => {
const client = net.connect(socketPath);
let data = '';
client.once('connect', () => {
client.write(JSON.stringify({ kill: true }) + '\n');
});
client.once('end', () => resolve());
client.once('error', () => resolve());
client.on('data', chunk => { data += chunk; });
client.once('end', () => {
let pid: number | undefined;
try { pid = JSON.parse(data.trim()).pid; } catch { }
if (pid === undefined)
reject(new Error('Dashboard did not return its PID'));
else
resolve(pid);
});
client.once('error', () => resolve(undefined));
});
if (pid === undefined)
return;
// Poll until the daemon process exits — at that point the OS has released all
// its handles, including the named pipe, so the next acquisition won't see a stale pipe.
const deadline = monotonicTime() + 35000;
while (monotonicTime() < deadline) {
try {
process.kill(pid, 0);
} catch {
return;
}
await new Promise(r => setTimeout(r, 50));
}
throw new Error(`Dashboard process ${pid} did not exit within the deadline`);
}

async function runAnnotateClient(options: DashboardOptions): Promise<void> {
Expand Down
4 changes: 2 additions & 2 deletions tests/mcp/cli-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export const test = baseTest.extend<{
return page;
});
},
connectToDashboard: async ({ cli, playwright }, use) => {
connectToDashboard: [async ({ cli, playwright }, use) => {
await use(async (bindTitle: string) => {
let endpoint = '';
await expect(async () => {
Expand All @@ -72,7 +72,7 @@ export const test = baseTest.extend<{
return await playwright.chromium.connect(endpoint);
});
await cli('show', '--kill');
},
}, { timeout: 60000 }],

cli: async ({ mcpBrowser, mcpHeadless, childProcess }, use) => {
await fs.promises.mkdir(test.info().outputPath('.playwright'), { recursive: true });
Expand Down
Loading