diff --git a/packages/playwright-core/src/tools/cli-daemon/daemon.ts b/packages/playwright-core/src/tools/cli-daemon/daemon.ts index 61f576a02fc95..85b5a73b978e6 100644 --- a/packages/playwright-core/src/tools/cli-daemon/daemon.ts +++ b/packages/playwright-core/src/tools/cli-daemon/daemon.ts @@ -45,6 +45,29 @@ async function socketExists(socketPath: string): Promise { return false; } +async function monitorSocketPath(socketPath: string): Promise { + if (process.platform === 'win32') + return; + + const socketStat = await fs.promises.stat(socketPath); + + async function checkSocketPath() { + const currentStat = await fs.promises.stat(socketPath).catch(() => undefined); + if (!currentStat || !currentStat.isSocket() || currentStat.dev !== socketStat.dev || currentStat.ino !== socketStat.ino) { + gracefullyProcessExitDoNotHang(0); + return; + } + scheduleSocketCheck(); + } + + function scheduleSocketCheck() { + const timer = setTimeout(() => void checkSocketPath(), 1000); + timer.unref(); + } + + scheduleSocketCheck(); +} + export async function startCliDaemonServer( sessionName: string, browserContext: playwright.BrowserContext, @@ -118,6 +141,7 @@ export async function startCliDaemonServer( }); await saveSessionFile(clientInfo, sessionConfig); + await monitorSocketPath(socketPath); return socketPath; } diff --git a/tests/mcp/cli-session.spec.ts b/tests/mcp/cli-session.spec.ts index 08a2b13125469..683623b88c9a9 100644 --- a/tests/mcp/cli-session.spec.ts +++ b/tests/mcp/cli-session.spec.ts @@ -15,11 +15,21 @@ */ import fs from 'fs'; +import os from 'os'; import path from 'path'; import { test, expect, daemonFolder } from './cli-fixtures'; import { killProcessGroup } from '../config/commonFixtures'; import playwright from '../../packages/playwright-core'; +function isProcessAlive(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +} + test('list', async ({ cli, server }) => { const { output: emptyOutput } = await cli('list'); expect(emptyOutput).toContain('(no browsers)'); @@ -110,6 +120,30 @@ test('session stops when browser exits', async ({ cli, server }) => { expect(listAfter).toContain('(no browsers)'); }); +test('session stops when temporary socket directory disappears', async ({ cli, server }) => { + test.skip(process.platform === 'win32'); + + // Keep this short because macOS limits Unix socket paths to 103 bytes. + const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'p-')); + const removedTempDir = tempDir + '.removed'; + try { + const { daemonPid } = await cli('open', server.HELLO_WORLD, { + env: { + PWTEST_SOCKETS_DIR: '', + TMPDIR: tempDir, + }, + }); + expect(daemonPid).toBeDefined(); + expect(isProcessAlive(daemonPid)).toBe(true); + + await fs.promises.rename(tempDir, removedTempDir); + await expect.poll(() => isProcessAlive(daemonPid)).toBe(false); + } finally { + await fs.promises.rm(tempDir, { force: true, recursive: true }); + await fs.promises.rm(removedTempDir, { force: true, recursive: true }); + } +}); + test('session reopen with different config', async ({ cli, server }, testInfo) => { const config = { browser: { contextOptions: { viewport: { width: 700, height: 500 } } } }; const configPath = testInfo.outputPath('config.json');