Skip to content

Commit e2fe412

Browse files
committed
refactor: add support for CLI sessionIds in tests
1 parent 57648b7 commit e2fe412

11 files changed

Lines changed: 131 additions & 93 deletions

scripts/test.mjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ const nodeArgs = [
7070
'--test-reporter',
7171
(process.env['NODE_TEST_REPORTER'] ?? process.env['CI']) ? 'spec' : 'dot',
7272
'--test-force-exit',
73-
'--test-concurrency=1',
7473
'--test',
7574
'--test-timeout=120000',
7675
...flags,

src/bin/chrome-devtools.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ await checkForUpdates(
3131
'Run `npm install -g chrome-devtools-mcp@latest` and `chrome-devtools start` to update and restart the daemon.',
3232
);
3333

34-
async function start(args: string[]) {
34+
async function start(args: string[], sessionId: string) {
3535
const combinedArgs = [...args, ...defaultArgs];
36-
await startDaemon(combinedArgs);
36+
await startDaemon(combinedArgs, sessionId);
3737
logDisclaimers(parseArguments(VERSION, combinedArgs));
3838
}
3939

@@ -78,6 +78,12 @@ const y = yargs(hideBin(process.argv))
7878
.usage(
7979
`Run 'chrome-devtools <command> --help' for help on the specific command.`,
8080
)
81+
.option('sessionId', {
82+
type: 'string',
83+
description: 'Session ID for daemon scoping',
84+
default: '',
85+
hidden: true,
86+
})
8187
.demandCommand()
8288
.version(VERSION)
8389
.strict()
@@ -96,7 +102,7 @@ y.command(
96102
)
97103
.strict(),
98104
async argv => {
99-
if (isDaemonRunning()) {
105+
if (isDaemonRunning(argv.sessionId)) {
100106
await stopDaemon();
101107
}
102108
// Defaults but we do not want to affect the yargs conflict resolution.
@@ -107,13 +113,13 @@ y.command(
107113
argv.headless = true;
108114
}
109115
const args = serializeArgs(cliOptions, argv);
110-
await start(args);
116+
await start(args, argv.sessionId);
111117
process.exit(0);
112118
},
113119
).strict(); // Re-enable strict validation for other commands; this is applied to the yargs instance itself
114120

115-
y.command('status', 'Checks if chrome-devtools-mcp is running', async () => {
116-
if (isDaemonRunning()) {
121+
y.command('status', 'Checks if chrome-devtools-mcp is running', y => y, async argv => {
122+
if (isDaemonRunning(argv.sessionId)) {
117123
console.log('chrome-devtools-mcp daemon is running.');
118124
const response = await sendCommand({
119125
method: 'status',
@@ -140,11 +146,12 @@ y.command('status', 'Checks if chrome-devtools-mcp is running', async () => {
140146
process.exit(0);
141147
});
142148

143-
y.command('stop', 'Stop chrome-devtools-mcp if any', async () => {
144-
if (!isDaemonRunning()) {
149+
y.command('stop', 'Stop chrome-devtools-mcp if any', y => y, async argv => {
150+
const sessionId = argv.sessionId as string;
151+
if (!isDaemonRunning(sessionId)) {
145152
process.exit(0);
146153
}
147-
await stopDaemon();
154+
await stopDaemon(sessionId);
148155
process.exit(0);
149156
});
150157

@@ -213,9 +220,10 @@ for (const [commandName, commandDef] of Object.entries(commands)) {
213220
}
214221
},
215222
async argv => {
223+
const sessionId = argv.sessionId as string;
216224
try {
217-
if (!isDaemonRunning()) {
218-
await start([]);
225+
if (!isDaemonRunning(sessionId)) {
226+
await start([], sessionId);
219227
}
220228

221229
const commandArgs: Record<string, unknown> = {};
@@ -229,7 +237,7 @@ for (const [commandName, commandDef] of Object.entries(commands)) {
229237
method: 'invoke_tool',
230238
tool: commandName,
231239
args: commandArgs,
232-
});
240+
}, sessionId);
233241

234242
if (response.success) {
235243
console.log(

src/daemon/client.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,13 @@ function waitForFile(filePath: string, removed = false) {
6767
});
6868
}
6969

70-
export async function startDaemon(mcpArgs: string[] = []) {
71-
if (isDaemonRunning()) {
70+
export async function startDaemon(mcpArgs: string[] = [], sessionId = '') {
71+
if (isDaemonRunning(sessionId)) {
7272
logger('Daemon is already running');
7373
return;
7474
}
7575

76-
const pidFilePath = getPidFilePath();
76+
const pidFilePath = getPidFilePath(sessionId);
7777

7878
if (fs.existsSync(pidFilePath)) {
7979
fs.unlinkSync(pidFilePath);
@@ -83,7 +83,7 @@ export async function startDaemon(mcpArgs: string[] = []) {
8383
const child = spawn(process.execPath, [DAEMON_SCRIPT_PATH, ...mcpArgs], {
8484
detached: true,
8585
stdio: 'ignore',
86-
env: process.env,
86+
env: { ...process.env, CHROME_DEVTOOLS_MCP_SESSION_ID: sessionId },
8787
cwd: process.cwd(),
8888
windowsHide: true,
8989
});
@@ -99,8 +99,9 @@ const SEND_COMMAND_TIMEOUT = 60_000; // ms
9999
*/
100100
export async function sendCommand(
101101
command: DaemonMessage,
102+
sessionId = ''
102103
): Promise<DaemonResponse> {
103-
const socketPath = getSocketPath();
104+
const socketPath = getSocketPath(sessionId);
104105

105106
const socket = net.createConnection({
106107
path: socketPath,
@@ -133,15 +134,15 @@ export async function sendCommand(
133134
});
134135
}
135136

136-
export async function stopDaemon() {
137-
if (!isDaemonRunning()) {
137+
export async function stopDaemon(sessionId = '') {
138+
if (!isDaemonRunning(sessionId)) {
138139
logger('Daemon is not running');
139140
return;
140141
}
141142

142-
const pidFilePath = getPidFilePath();
143+
const pidFilePath = getPidFilePath(sessionId);
143144

144-
await sendCommand({method: 'stop'});
145+
await sendCommand({method: 'stop'}, sessionId);
145146

146147
await waitForFile(pidFilePath, /*removed=*/ true);
147148
}

src/daemon/daemon.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,27 @@ import {VERSION} from '../version.js';
2222
import type {DaemonMessage} from './types.js';
2323
import {
2424
DAEMON_CLIENT_NAME,
25-
getDaemonPid,
2625
getPidFilePath,
2726
getSocketPath,
2827
INDEX_SCRIPT_PATH,
2928
IS_WINDOWS,
3029
isDaemonRunning,
3130
} from './utils.js';
3231

33-
const pid = getDaemonPid();
34-
if (isDaemonRunning(pid)) {
32+
const sessionId = process.env.CHROME_DEVTOOLS_MCP_SESSION_ID || '';
33+
logger(`Daemon sessionId: ${sessionId}`);
34+
if (isDaemonRunning(sessionId)) {
3535
logger('Another daemon process is running.');
3636
process.exit(1);
3737
}
38-
const pidFilePath = getPidFilePath();
38+
const pidFilePath = getPidFilePath(sessionId);
3939
fs.mkdirSync(path.dirname(pidFilePath), {
4040
recursive: true,
4141
});
4242
fs.writeFileSync(pidFilePath, process.pid.toString());
4343
logger(`Writing ${process.pid.toString()} to ${pidFilePath}`);
4444

45-
const socketPath = getSocketPath();
45+
const socketPath = getSocketPath(sessionId);
4646

4747
const startDate = new Date();
4848
const mcpServerArgs = process.argv.slice(2);

src/daemon/utils.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,56 +24,60 @@ const APP_NAME = 'chrome-devtools-mcp';
2424
export const DAEMON_CLIENT_NAME = 'chrome-devtools-cli-daemon';
2525

2626
// Using these paths due to strict limits on the POSIX socket path length.
27-
export function getSocketPath(): string {
27+
export function getSocketPath(sessionId: string): string {
2828
const uid = os.userInfo().uid;
29+
const suffix = sessionId ? `-${sessionId}` : '';
30+
const appName = APP_NAME + suffix;
2931

3032
if (IS_WINDOWS) {
3133
// Windows uses Named Pipes, not file paths.
3234
// This format is required for server.listen()
33-
return path.join('\\\\.\\pipe', APP_NAME, 'server.sock');
35+
return path.join('\\\\.\\pipe', appName, 'server.sock');
3436
}
3537

3638
// 1. Try XDG_RUNTIME_DIR (Linux standard, sometimes macOS)
3739
if (process.env.XDG_RUNTIME_DIR) {
38-
return path.join(process.env.XDG_RUNTIME_DIR, APP_NAME, 'server.sock');
40+
return path.join(process.env.XDG_RUNTIME_DIR, appName, 'server.sock');
3941
}
4042

4143
// 2. macOS/Unix Fallback: Use /tmp/
4244
// We use /tmp/ because it is much shorter than ~/Library/Application Support/
4345
// and keeps us well under the 104-character limit.
44-
return path.join('/tmp', `${APP_NAME}-${uid}.sock`);
46+
return path.join('/tmp', `${appName}-${uid}.sock`);
4547
}
4648

47-
export function getRuntimeHome(): string {
49+
export function getRuntimeHome(sessionId: string): string {
4850
const platform = os.platform();
4951
const uid = os.userInfo().uid;
52+
const suffix = sessionId ? `-${sessionId}` : '';
53+
const appName = APP_NAME + suffix;
5054

5155
// 1. Check for the modern Unix standard
5256
if (process.env.XDG_RUNTIME_DIR) {
53-
return path.join(process.env.XDG_RUNTIME_DIR, APP_NAME);
57+
return path.join(process.env.XDG_RUNTIME_DIR, appName);
5458
}
5559

5660
// 2. Fallback for macOS and older Linux
5761
if (platform === 'darwin' || platform === 'linux') {
5862
// /tmp is cleared on boot, making it perfect for PIDs
59-
return path.join('/tmp', `${APP_NAME}-${uid}`);
63+
return path.join('/tmp', `${appName}-${uid}`);
6064
}
6165

6266
// 3. Windows Fallback
63-
return path.join(os.tmpdir(), APP_NAME);
67+
return path.join(os.tmpdir(), appName);
6468
}
6569

6670
export const IS_WINDOWS = os.platform() === 'win32';
6771

68-
export function getPidFilePath() {
69-
const runtimeDir = getRuntimeHome();
72+
export function getPidFilePath(sessionId: string) {
73+
const runtimeDir = getRuntimeHome(sessionId);
7074
return path.join(runtimeDir, 'daemon.pid');
7175
}
7276

73-
export function getDaemonPid() {
77+
export function getDaemonPid(sessionId: string) {
7478
try {
75-
const pidFile = getPidFilePath();
76-
logger(`Daemon pid file ${pidFile}`);
79+
const pidFile = getPidFilePath(sessionId);
80+
logger(`Daemon pid file ${pidFile} sessionId=${sessionId}`);
7781
if (!fs.existsSync(pidFile)) {
7882
return null;
7983
}
@@ -89,7 +93,8 @@ export function getDaemonPid() {
8993
}
9094
}
9195

92-
export function isDaemonRunning(pid = getDaemonPid()): pid is number {
96+
export function isDaemonRunning(sessionId: string): boolean {
97+
const pid = getDaemonPid(sessionId);
9398
if (pid) {
9499
try {
95100
process.kill(pid, 0); // Throws if process doesn't exist

tests/daemon/client.test.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import assert from 'node:assert';
8+
import crypto from 'node:crypto';
89
import {describe, it, afterEach, beforeEach} from 'node:test';
910

1011
import {
@@ -16,39 +17,42 @@ import {isDaemonRunning} from '../../src/daemon/utils.js';
1617

1718
describe('daemon client', () => {
1819
describe('start/stop', () => {
20+
let sessionId: string;
21+
1922
beforeEach(async () => {
20-
await stopDaemon();
23+
sessionId = crypto.randomUUID();
24+
await stopDaemon(sessionId);
2125
});
2226

2327
afterEach(async () => {
24-
await stopDaemon();
28+
await stopDaemon(sessionId);
2529
});
2630

2731
it('should start and stop daemon', async () => {
28-
assert.ok(!isDaemonRunning(), 'Daemon should not be running initially');
32+
assert.ok(!isDaemonRunning(sessionId), 'Daemon should not be running initially');
2933

30-
await startDaemon();
31-
assert.ok(isDaemonRunning(), 'Daemon should be running after start');
34+
await startDaemon([], sessionId);
35+
assert.ok(isDaemonRunning(sessionId), 'Daemon should be running after start');
3236

33-
await stopDaemon();
34-
assert.ok(!isDaemonRunning(), 'Daemon should not be running after stop');
37+
await stopDaemon(sessionId);
38+
assert.ok(!isDaemonRunning(sessionId), 'Daemon should not be running after stop');
3539
});
3640

3741
it('should handle starting daemon when already running', async () => {
38-
await startDaemon();
39-
assert.ok(isDaemonRunning(), 'Daemon should be running');
42+
await startDaemon([], sessionId);
43+
assert.ok(isDaemonRunning(sessionId), 'Daemon should be running');
4044

4145
// Starting again should be a no-op
42-
await startDaemon();
43-
assert.ok(isDaemonRunning(), 'Daemon should still be running');
46+
await startDaemon([], sessionId);
47+
assert.ok(isDaemonRunning(sessionId), 'Daemon should still be running');
4448
});
4549

4650
it('should handle stopping daemon when not running', async () => {
47-
assert.ok(!isDaemonRunning(), 'Daemon should not be running initially');
51+
assert.ok(!isDaemonRunning(sessionId), 'Daemon should not be running initially');
4852

4953
// Stopping when not running should be a no-op
50-
await stopDaemon();
51-
assert.ok(!isDaemonRunning(), 'Daemon should still not be running');
54+
await stopDaemon(sessionId);
55+
assert.ok(!isDaemonRunning(sessionId), 'Daemon should still not be running');
5256
});
5357
});
5458

0 commit comments

Comments
 (0)