Skip to content

Commit de13224

Browse files
authored
chore(dashboard): report daemon PID via reveal ack (#40906)
1 parent 93ec56f commit de13224

3 files changed

Lines changed: 36 additions & 13 deletions

File tree

packages/playwright-core/src/tools/cli-client/program.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,13 +244,17 @@ export async function program(options?: { embedderVersion?: string}) {
244244
}
245245
const timer = setTimeout(() => child.stdin!.destroy(), 60_000);
246246
child.unref();
247+
let daemonPid: number;
247248
try {
248249
await new Promise<void>((resolve, reject) => {
249250
let outLog = '';
250251
child.stdout!.on('data', data => {
251252
outLog += data.toString();
252-
if (outLog.includes('Dashboard is running'))
253+
const match = outLog.match(/Dashboard is running pid=(\d+)/);
254+
if (match) {
255+
daemonPid = Number(match[1]);
253256
resolve();
257+
}
254258
});
255259
child.once('exit', (code, signal) => reject(new Error(`Dashboard daemon exited (code=${code}, signal=${signal}) before signaling READY${outLog ? '\n' + outLog : ''}`)));
256260
});
@@ -260,7 +264,7 @@ export async function program(options?: { embedderVersion?: string}) {
260264
child.stdin!.destroy();
261265
child.stdout!.destroy();
262266
}
263-
output.show(sessionName, child.pid);
267+
output.show(sessionName, daemonPid!);
264268
return;
265269
}
266270
default: {

packages/playwright-core/src/tools/dashboard/dashboardApp.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -253,25 +253,38 @@ function parseOpenArgs(): DashboardOptions {
253253
};
254254
}
255255

256-
async function acquireSingleton(options: DashboardOptions): Promise<net.Server> {
256+
type AcquireResult =
257+
| { role: 'winner', server: net.Server }
258+
| { role: 'loser', daemonPid: number };
259+
260+
async function acquireSingleton(options: DashboardOptions): Promise<AcquireResult> {
257261
const socketPath = dashboardSocketPath();
258262
if (process.platform !== 'win32')
259263
await fs.promises.mkdir(path.dirname(socketPath), { recursive: true });
260264

261265
return await new Promise((resolve, reject) => {
262266
const server = net.createServer();
263-
server.listen(socketPath, () => resolve(server));
267+
server.listen(socketPath, () => resolve({ role: 'winner', server }));
264268
server.on('error', (err: NodeJS.ErrnoException) => {
265269
if (err.code !== 'EADDRINUSE')
266270
return reject(err);
271+
let ackBuffer = '';
267272
const client = net.connect(socketPath, () => {
268273
client.write(JSON.stringify(options) + '\n');
269274
});
270-
client.on('end', () => reject(new Error('already running')));
275+
client.on('data', chunk => { ackBuffer += chunk.toString(); });
276+
client.on('end', () => {
277+
try {
278+
const { pid } = JSON.parse(ackBuffer.trim());
279+
resolve({ role: 'loser', daemonPid: pid });
280+
} catch (e) {
281+
reject(e);
282+
}
283+
});
271284
client.on('error', () => {
272285
if (process.platform !== 'win32')
273286
fs.unlinkSync(socketPath);
274-
server.listen(socketPath, () => resolve(server));
287+
server.listen(socketPath, () => resolve({ role: 'winner', server }));
275288
});
276289
});
277290
});
@@ -304,24 +317,23 @@ export async function openDashboardApp() {
304317
// Self-destruct if the parent CLI dies before we signal READY. Unregistered
305318
// before we signal so the daemon outlives the parent.
306319
const stopSelfDestruct = selfDestructOnParentGone();
307-
let server: net.Server;
308-
try {
309-
server = await acquireSingleton(options);
310-
} catch {
320+
const acquired = await acquireSingleton(options);
321+
if (acquired.role === 'loser') {
311322
// Another daemon is already running, signal success.
312323
stopSelfDestruct();
313324
// eslint-disable-next-line no-console
314-
console.log('Dashboard is running');
325+
console.log(`Dashboard is running pid=${acquired.daemonPid}`);
315326
// eslint-disable-next-line no-restricted-properties
316327
await new Promise(f => process.stdout.write('', f)); // Make sure stdout is flushed.
317328
return;
318329
}
330+
const { server } = acquired;
319331
process.on('exit', () => server.close());
320332
try {
321333
await startApp(server, options);
322334
stopSelfDestruct();
323335
// eslint-disable-next-line no-console
324-
console.log('Dashboard is running');
336+
console.log(`Dashboard is running pid=${process.pid}`);
325337
} catch (error) {
326338
// eslint-disable-next-line no-console
327339
console.log(error);
@@ -366,7 +378,7 @@ async function startApp(server: net.Server, options: DashboardOptions) {
366378
try {
367379
await page?.bringToFront();
368380
await dashboard.reveal(parsed);
369-
socket.end();
381+
socket.end(JSON.stringify({ pid: process.pid }) + '\n');
370382
} catch (e) {
371383
socket.end(e);
372384
}

tests/mcp/dashboard.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,10 @@ test('save recording streams WebM bytes to the chosen file', async ({ cli, serve
178178
// WebM files start with the EBML magic bytes.
179179
expect(bytes.subarray(0, 4)).toEqual(Buffer.from([0x1a, 0x45, 0xdf, 0xa3]));
180180
});
181+
182+
test('two concurrent cli show invocations both succeed', async ({ cli }) => {
183+
const bindTitle = `--playwright-internal--${crypto.randomUUID()}`;
184+
const [first, second] = await Promise.all([cli('show', { bindTitle }), cli('show', { bindTitle })]);
185+
expect(first.dashboardPid).toBe(second.dashboardPid);
186+
await cli('show', '--kill');
187+
});

0 commit comments

Comments
 (0)