Skip to content

Commit 82b67b0

Browse files
authored
refactor: add support for CLI sessionIds in tests (#1919)
Closes #1119
1 parent a1612be commit 82b67b0

11 files changed

Lines changed: 195 additions & 123 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: 66 additions & 42 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,8 +102,8 @@ y.command(
96102
)
97103
.strict(),
98104
async argv => {
99-
if (isDaemonRunning()) {
100-
await stopDaemon();
105+
if (isDaemonRunning(argv.sessionId)) {
106+
await stopDaemon(argv.sessionId);
101107
}
102108
// Defaults but we do not want to affect the yargs conflict resolution.
103109
if (argv.isolated === undefined && argv.userDataDir === undefined) {
@@ -107,46 +113,60 @@ 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()) {
117-
console.log('chrome-devtools-mcp daemon is running.');
118-
const response = await sendCommand({
119-
method: 'status',
120-
});
121-
if (response.success) {
122-
const data = JSON.parse(response.result) as {
123-
pid: number | null;
124-
socketPath: string;
125-
startDate: string;
126-
version: string;
127-
args: string[];
128-
};
129-
console.log(
130-
`pid=${data.pid} socket=${data.socketPath} start-date=${data.startDate} version=${data.version}`,
121+
y.command(
122+
'status',
123+
'Checks if chrome-devtools-mcp is running',
124+
y => y,
125+
async argv => {
126+
if (isDaemonRunning(argv.sessionId)) {
127+
console.log('chrome-devtools-mcp daemon is running.');
128+
const response = await sendCommand(
129+
{
130+
method: 'status',
131+
},
132+
argv.sessionId,
131133
);
132-
console.log(`args=${JSON.stringify(data.args)}`);
134+
if (response.success) {
135+
const data = JSON.parse(response.result) as {
136+
pid: number | null;
137+
socketPath: string;
138+
startDate: string;
139+
version: string;
140+
args: string[];
141+
};
142+
console.log(
143+
`pid=${data.pid} socket=${data.socketPath} start-date=${data.startDate} version=${data.version}`,
144+
);
145+
console.log(`args=${JSON.stringify(data.args)}`);
146+
} else {
147+
console.error('Error:', response.error);
148+
process.exit(1);
149+
}
133150
} else {
134-
console.error('Error:', response.error);
135-
process.exit(1);
151+
console.log('chrome-devtools-mcp daemon is not running.');
136152
}
137-
} else {
138-
console.log('chrome-devtools-mcp daemon is not running.');
139-
}
140-
process.exit(0);
141-
});
153+
process.exit(0);
154+
},
155+
);
142156

143-
y.command('stop', 'Stop chrome-devtools-mcp if any', async () => {
144-
if (!isDaemonRunning()) {
157+
y.command(
158+
'stop',
159+
'Stop chrome-devtools-mcp if any',
160+
y => y,
161+
async argv => {
162+
const sessionId = argv.sessionId as string;
163+
if (!isDaemonRunning(sessionId)) {
164+
process.exit(0);
165+
}
166+
await stopDaemon(sessionId);
145167
process.exit(0);
146-
}
147-
await stopDaemon();
148-
process.exit(0);
149-
});
168+
},
169+
);
150170

151171
for (const [commandName, commandDef] of Object.entries(commands)) {
152172
const args = commandDef.args;
@@ -213,9 +233,10 @@ for (const [commandName, commandDef] of Object.entries(commands)) {
213233
}
214234
},
215235
async argv => {
236+
const sessionId = argv.sessionId as string;
216237
try {
217-
if (!isDaemonRunning()) {
218-
await start([]);
238+
if (!isDaemonRunning(sessionId)) {
239+
await start([], sessionId);
219240
}
220241

221242
const commandArgs: Record<string, unknown> = {};
@@ -225,11 +246,14 @@ for (const [commandName, commandDef] of Object.entries(commands)) {
225246
}
226247
}
227248

228-
const response = await sendCommand({
229-
method: 'invoke_tool',
230-
tool: commandName,
231-
args: commandArgs,
232-
});
249+
const response = await sendCommand(
250+
{
251+
method: 'invoke_tool',
252+
tool: commandName,
253+
args: commandArgs,
254+
},
255+
sessionId,
256+
);
233257

234258
if (response.success) {
235259
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: string) {
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: string,
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: string) {
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

0 commit comments

Comments
 (0)