@romanmatena/browsermonitor 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +558 -0
- package/package.json +53 -0
- package/src/agents.llm/browser-monitor-section.md +18 -0
- package/src/cli.mjs +202 -0
- package/src/http-server.mjs +536 -0
- package/src/init.mjs +162 -0
- package/src/intro.mjs +36 -0
- package/src/logging/LogBuffer.mjs +178 -0
- package/src/logging/constants.mjs +19 -0
- package/src/logging/dump.mjs +207 -0
- package/src/logging/index.mjs +13 -0
- package/src/logging/timestamps.mjs +13 -0
- package/src/monitor/README.md +10 -0
- package/src/monitor/index.mjs +18 -0
- package/src/monitor/interactive-mode.mjs +275 -0
- package/src/monitor/join-mode.mjs +654 -0
- package/src/monitor/open-mode.mjs +889 -0
- package/src/monitor/page-monitoring.mjs +199 -0
- package/src/monitor/tab-selection.mjs +53 -0
- package/src/monitor.mjs +39 -0
- package/src/os/README.md +4 -0
- package/src/os/wsl/chrome.mjs +503 -0
- package/src/os/wsl/detect.mjs +68 -0
- package/src/os/wsl/diagnostics.mjs +729 -0
- package/src/os/wsl/index.mjs +45 -0
- package/src/os/wsl/port-proxy.mjs +190 -0
- package/src/settings.mjs +101 -0
- package/src/templates/api-help.mjs +212 -0
- package/src/templates/cli-commands.mjs +51 -0
- package/src/templates/interactive-keys.mjs +33 -0
- package/src/templates/ready-help.mjs +33 -0
- package/src/templates/section-heading.mjs +141 -0
- package/src/templates/table-helper.mjs +73 -0
- package/src/templates/wait-for-chrome.mjs +19 -0
- package/src/utils/ask.mjs +49 -0
- package/src/utils/chrome-profile-path.mjs +37 -0
- package/src/utils/colors.mjs +49 -0
- package/src/utils/env.mjs +30 -0
- package/src/utils/profile-id.mjs +23 -0
- package/src/utils/status-line.mjs +47 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome detection and launch utilities for WSL→Windows.
|
|
3
|
+
*
|
|
4
|
+
* Chrome singleton pattern: when Chrome is already running, new Chrome instances
|
|
5
|
+
* send their arguments to the existing process via IPC and exit immediately.
|
|
6
|
+
* This means --remote-debugging-port flags on new launches are IGNORED.
|
|
7
|
+
*
|
|
8
|
+
* Chrome Canary is preferred for browsermonitor because it runs as a separate
|
|
9
|
+
* process from regular Chrome, avoiding singleton conflicts.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { getProfileIdFromProjectDir } from '../../utils/profile-id.mjs';
|
|
15
|
+
import { execFileSync, execSync, spawnSync } from 'child_process';
|
|
16
|
+
import { C, log } from '../../utils/colors.mjs';
|
|
17
|
+
|
|
18
|
+
/** Last stderr lines from cmd.exe (UNC/CMD warnings). Cleared when read. */
|
|
19
|
+
let _lastCmdStderrLines = [];
|
|
20
|
+
|
|
21
|
+
/** Run cmd.exe and capture stderr; store lines for caller to format (do not print). */
|
|
22
|
+
function execCmdAndFormatStderr(cmdAfterSlashC) {
|
|
23
|
+
const result = spawnSync('cmd.exe', ['/c', cmdAfterSlashC], {
|
|
24
|
+
encoding: 'utf8',
|
|
25
|
+
timeout: 5000,
|
|
26
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
27
|
+
});
|
|
28
|
+
const stderr = (result.stderr || '').trim();
|
|
29
|
+
if (stderr) {
|
|
30
|
+
_lastCmdStderrLines = stderr.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
31
|
+
} else {
|
|
32
|
+
_lastCmdStderrLines = [];
|
|
33
|
+
}
|
|
34
|
+
if (result.status !== 0) throw new Error(stderr || 'cmd failed');
|
|
35
|
+
return (result.stdout || '').trim().replace(/\r?\n/g, '');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Get and clear stored cmd stderr lines (for formatting by caller). */
|
|
39
|
+
export function getLastCmdStderrAndClear() {
|
|
40
|
+
const lines = _lastCmdStderrLines;
|
|
41
|
+
_lastCmdStderrLines = [];
|
|
42
|
+
return lines;
|
|
43
|
+
}
|
|
44
|
+
import { getWslDistroName } from './detect.mjs';
|
|
45
|
+
|
|
46
|
+
// Cache for LOCALAPPDATA path
|
|
47
|
+
let _cachedLocalAppData = null;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get Windows LOCALAPPDATA path (cached).
|
|
51
|
+
* @returns {string} Expanded LOCALAPPDATA path (e.g., C:\Users\info\AppData\Local)
|
|
52
|
+
*/
|
|
53
|
+
export function getWindowsLocalAppData() {
|
|
54
|
+
if (_cachedLocalAppData) return _cachedLocalAppData;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// Get LOCALAPPDATA from Windows - use cmd.exe to expand the variable
|
|
58
|
+
_cachedLocalAppData = execCmdAndFormatStderr('echo %LOCALAPPDATA%');
|
|
59
|
+
} catch {
|
|
60
|
+
// Fallback: try to get username and build path
|
|
61
|
+
try {
|
|
62
|
+
const winUser = execCmdAndFormatStderr('echo %USERNAME%');
|
|
63
|
+
_cachedLocalAppData = `C:\\Users\\${winUser}\\AppData\\Local`;
|
|
64
|
+
} catch {
|
|
65
|
+
// Last resort fallback
|
|
66
|
+
_cachedLocalAppData = 'C:\\Users\\Public\\AppData\\Local';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return _cachedLocalAppData;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Generate a unique Windows profile path for a project.
|
|
75
|
+
* Uses local Windows directory instead of UNC path for better Chrome compatibility.
|
|
76
|
+
* Delegates to getProfileIdFromProjectDir for consistent projectName + hash.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} projectDir - WSL project directory path
|
|
79
|
+
* @returns {string} Windows-style profile path (in LOCALAPPDATA\browsermonitor\)
|
|
80
|
+
*/
|
|
81
|
+
export function getWindowsProfilePath(projectDir) {
|
|
82
|
+
const { profileId } = getProfileIdFromProjectDir(projectDir);
|
|
83
|
+
const localAppData = getWindowsLocalAppData();
|
|
84
|
+
return `${localAppData}\\browsermonitor\\${profileId}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Detect Chrome Canary installation path from WSL.
|
|
89
|
+
* Chrome Canary is preferred for browsermonitor because it runs as a separate
|
|
90
|
+
* process from regular Chrome, avoiding singleton conflicts.
|
|
91
|
+
*
|
|
92
|
+
* @returns {string|null} Windows-style path to Chrome Canary or null if not found
|
|
93
|
+
*/
|
|
94
|
+
export function detectWindowsChromeCanaryPath() {
|
|
95
|
+
try {
|
|
96
|
+
const usersDir = '/mnt/c/Users';
|
|
97
|
+
if (fs.existsSync(usersDir)) {
|
|
98
|
+
const users = fs.readdirSync(usersDir).filter(u =>
|
|
99
|
+
!['Default', 'Default User', 'Public', 'All Users'].includes(u) &&
|
|
100
|
+
fs.statSync(path.join(usersDir, u)).isDirectory()
|
|
101
|
+
);
|
|
102
|
+
for (const user of users) {
|
|
103
|
+
const canaryPath = `/mnt/c/Users/${user}/AppData/Local/Google/Chrome SxS/Application/chrome.exe`;
|
|
104
|
+
const winPath = `C:\\Users\\${user}\\AppData\\Local\\Google\\Chrome SxS\\Application\\chrome.exe`;
|
|
105
|
+
try {
|
|
106
|
+
if (fs.existsSync(canaryPath)) {
|
|
107
|
+
return winPath;
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// Ignore access errors
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// Ignore errors reading user directories
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Detect Chrome installation path from WSL.
|
|
122
|
+
* For LAUNCH mode, prefers Chrome Canary (isolated from user's regular Chrome).
|
|
123
|
+
* For CONNECT mode, this function is not used (connects to any running Chrome).
|
|
124
|
+
*
|
|
125
|
+
* @param {boolean} canaryOnly - If true, only return Canary path (for launch mode)
|
|
126
|
+
* @returns {string|null} Windows-style path to Chrome or null if not found
|
|
127
|
+
*/
|
|
128
|
+
export function detectWindowsChromePath(canaryOnly = false) {
|
|
129
|
+
const canaryPath = detectWindowsChromeCanaryPath();
|
|
130
|
+
if (canaryPath) {
|
|
131
|
+
return canaryPath;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (canaryOnly) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const chromePaths = [
|
|
139
|
+
{ wsl: '/mnt/c/Program Files/Google/Chrome/Application/chrome.exe', win: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' },
|
|
140
|
+
{ wsl: '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe', win: 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe' },
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
// Check user-specific installation (LOCALAPPDATA)
|
|
144
|
+
try {
|
|
145
|
+
const usersDir = '/mnt/c/Users';
|
|
146
|
+
if (fs.existsSync(usersDir)) {
|
|
147
|
+
const users = fs.readdirSync(usersDir).filter(u =>
|
|
148
|
+
!['Default', 'Default User', 'Public', 'All Users'].includes(u) &&
|
|
149
|
+
fs.statSync(path.join(usersDir, u)).isDirectory()
|
|
150
|
+
);
|
|
151
|
+
for (const user of users) {
|
|
152
|
+
const localAppDataPath = `/mnt/c/Users/${user}/AppData/Local/Google/Chrome/Application/chrome.exe`;
|
|
153
|
+
const winPath = `C:\\Users\\${user}\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe`;
|
|
154
|
+
chromePaths.push({ wsl: localAppDataPath, win: winPath });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// Ignore errors reading user directories
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (const p of chromePaths) {
|
|
162
|
+
try {
|
|
163
|
+
if (fs.existsSync(p.wsl)) {
|
|
164
|
+
return p.win;
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
// Ignore access errors
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Print Chrome Canary installation instructions.
|
|
176
|
+
* Called when Canary is not found and launch mode is requested.
|
|
177
|
+
*/
|
|
178
|
+
export function printCanaryInstallInstructions() {
|
|
179
|
+
console.log('');
|
|
180
|
+
console.log(`${C.yellow}═══════════════════════════════════════════════════════════════════════════════${C.reset}`);
|
|
181
|
+
console.log(`${C.bold}${C.yellow} CHROME CANARY REQUIRED${C.reset}`);
|
|
182
|
+
console.log(`${C.yellow}═══════════════════════════════════════════════════════════════════════════════${C.reset}`);
|
|
183
|
+
console.log('');
|
|
184
|
+
console.log(` For ${C.cyan}launch mode${C.reset}, browsermonitor requires ${C.brightGreen}Chrome Canary${C.reset}.`);
|
|
185
|
+
console.log('');
|
|
186
|
+
console.log(` ${C.bold}Why Chrome Canary?${C.reset}`);
|
|
187
|
+
console.log(` • Runs as a ${C.green}separate process${C.reset} from your regular Chrome`);
|
|
188
|
+
console.log(` • No singleton conflicts - your regular Chrome stays untouched`);
|
|
189
|
+
console.log(` • Debug port is guaranteed to work without port proxy`);
|
|
190
|
+
console.log('');
|
|
191
|
+
console.log(` ${C.bold}Installation:${C.reset}`);
|
|
192
|
+
console.log(` 1. Download from: ${C.brightCyan}https://www.google.com/chrome/canary/${C.reset}`);
|
|
193
|
+
console.log(` 2. Install normally (will NOT replace your regular Chrome)`);
|
|
194
|
+
console.log(` 3. Run browsermonitor again`);
|
|
195
|
+
console.log('');
|
|
196
|
+
console.log(` ${C.dim}Alternative: Use ${C.cyan}--join=9222${C.reset}${C.dim} to attach to any running Chrome with debug port.${C.reset}`);
|
|
197
|
+
console.log('');
|
|
198
|
+
console.log(`${C.yellow}═══════════════════════════════════════════════════════════════════════════════${C.reset}`);
|
|
199
|
+
console.log('');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Scan all Chrome instances on Windows for debug ports and profiles.
|
|
204
|
+
*
|
|
205
|
+
* @returns {{instances: Array<{port: number, profile: string, bindAddress: string}>, chromeRunning: boolean}}
|
|
206
|
+
*/
|
|
207
|
+
export function scanChromeInstances() {
|
|
208
|
+
try {
|
|
209
|
+
// Check if Chrome is running at all
|
|
210
|
+
const chromePid = execSync(
|
|
211
|
+
'powershell.exe -NoProfile -Command "Get-Process chrome -ErrorAction SilentlyContinue | Select-Object -First 1 | ForEach-Object { \\$_.Id }"',
|
|
212
|
+
{ encoding: 'utf8', timeout: 5000 }
|
|
213
|
+
).trim();
|
|
214
|
+
|
|
215
|
+
if (!chromePid) {
|
|
216
|
+
return { instances: [], chromeRunning: false };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Get all Chrome command lines with WMI
|
|
220
|
+
const wmicOutput = execSync(
|
|
221
|
+
'wmic.exe process where "name=\'chrome.exe\'" get commandline /format:list 2>nul',
|
|
222
|
+
{ encoding: 'utf8', timeout: 10000 }
|
|
223
|
+
).trim();
|
|
224
|
+
|
|
225
|
+
const instances = [];
|
|
226
|
+
const seenPorts = new Set();
|
|
227
|
+
|
|
228
|
+
if (wmicOutput) {
|
|
229
|
+
const lines = wmicOutput.split('\n').filter(l => l.includes('--remote-debugging-port'));
|
|
230
|
+
for (const line of lines) {
|
|
231
|
+
const portMatch = line.match(/--remote-debugging-port=(\d+)/);
|
|
232
|
+
const addressMatch = line.match(/--remote-debugging-address=([^\s'"]+)/);
|
|
233
|
+
const profileMatch = line.match(/--user-data-dir=(?:"([^"]+)"|'([^']+)'|([^\s]+))/i);
|
|
234
|
+
|
|
235
|
+
if (portMatch) {
|
|
236
|
+
const portNum = parseInt(portMatch[1], 10);
|
|
237
|
+
// Deduplicate - Chrome has many subprocesses with same args
|
|
238
|
+
if (!seenPorts.has(portNum)) {
|
|
239
|
+
seenPorts.add(portNum);
|
|
240
|
+
const profilePath = profileMatch ? (profileMatch[1] || profileMatch[2] || profileMatch[3]) : 'default';
|
|
241
|
+
instances.push({
|
|
242
|
+
port: portNum,
|
|
243
|
+
profile: profilePath,
|
|
244
|
+
bindAddress: addressMatch ? addressMatch[1] : '127.0.0.1',
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return { instances, chromeRunning: true };
|
|
252
|
+
} catch (e) {
|
|
253
|
+
return { instances: [], chromeRunning: false };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Find Chrome instance matching the current project's profile.
|
|
259
|
+
*
|
|
260
|
+
* @param {Array} instances - Chrome instances from scanChromeInstances()
|
|
261
|
+
* @param {string} projectDir - Current project directory (cwd)
|
|
262
|
+
* @returns {{found: boolean, instance: Object|null, matchType: string}}
|
|
263
|
+
*/
|
|
264
|
+
export function findProjectChrome(instances, projectDir) {
|
|
265
|
+
if (!instances || instances.length === 0) {
|
|
266
|
+
return { found: false, instance: null, matchType: 'none' };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const { projectName, profileId: expectedProfileId } = getProfileIdFromProjectDir(projectDir);
|
|
270
|
+
|
|
271
|
+
for (const inst of instances) {
|
|
272
|
+
const instProfile = inst.profile.toLowerCase();
|
|
273
|
+
|
|
274
|
+
// Match format: browsermonitor\{projectName}_{hash}
|
|
275
|
+
if (instProfile.includes('browsermonitor') && instProfile.includes(expectedProfileId)) {
|
|
276
|
+
return { found: true, instance: inst, matchType: 'exact' };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Match by project name in browsermonitor profile
|
|
280
|
+
if (instProfile.includes(projectName.toLowerCase()) && instProfile.includes('browsermonitor')) {
|
|
281
|
+
return { found: true, instance: inst, matchType: 'legacy' };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// No exact match - return first accessible instance as fallback candidate
|
|
286
|
+
const accessibleInst = instances.find(i => i.bindAddress === '0.0.0.0');
|
|
287
|
+
if (accessibleInst) {
|
|
288
|
+
return { found: false, instance: accessibleInst, matchType: 'accessible' };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return { found: false, instance: instances[0], matchType: 'first' };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Find next available debug port starting from 9222.
|
|
296
|
+
*
|
|
297
|
+
* @param {Array} instances - Chrome instances from scanChromeInstances()
|
|
298
|
+
* @param {number} startPort - Starting port (default: 9222)
|
|
299
|
+
* @returns {number} Next available port
|
|
300
|
+
*/
|
|
301
|
+
export function findFreeDebugPort(instances, startPort = 9222) {
|
|
302
|
+
const usedPorts = new Set(instances.map(i => i.port));
|
|
303
|
+
for (let port = startPort; port < startPort + 100; port++) {
|
|
304
|
+
if (!usedPorts.has(port)) {
|
|
305
|
+
return port;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return startPort;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Start Chrome on Windows and set up port proxy for WSL access.
|
|
313
|
+
*
|
|
314
|
+
* Note: Chrome M113+ ignores --remote-debugging-address=0.0.0.0 for security.
|
|
315
|
+
* Chrome always binds to 127.0.0.1 or ::1, so we MUST use port proxy for WSL access.
|
|
316
|
+
*
|
|
317
|
+
* @param {string} chromePath - Windows path to Chrome
|
|
318
|
+
* @param {number} port - Debug port
|
|
319
|
+
* @param {string} profileDir - Windows-style profile directory path
|
|
320
|
+
* @returns {boolean} true if launched successfully
|
|
321
|
+
*/
|
|
322
|
+
export function startChromeOnWindows(chromePath, port, profileDir) {
|
|
323
|
+
try {
|
|
324
|
+
// First, remove any existing port proxy on this port
|
|
325
|
+
try {
|
|
326
|
+
execSync(
|
|
327
|
+
`powershell.exe -NoProfile -Command "netsh interface portproxy delete v4tov4 listenport=${port} listenaddress=0.0.0.0 2>\\$null"`,
|
|
328
|
+
{ encoding: 'utf8', timeout: 5000 }
|
|
329
|
+
);
|
|
330
|
+
} catch { /* ignore */ }
|
|
331
|
+
|
|
332
|
+
const args = [
|
|
333
|
+
`--remote-debugging-port=${port}`,
|
|
334
|
+
`--user-data-dir="${profileDir}"`,
|
|
335
|
+
].join("','");
|
|
336
|
+
|
|
337
|
+
const psCommand = `Start-Process -FilePath '${chromePath}' -ArgumentList '${args}'`;
|
|
338
|
+
execSync(`powershell.exe -NoProfile -Command "${psCommand}"`, { encoding: 'utf8', timeout: 10000 });
|
|
339
|
+
|
|
340
|
+
log.dim('Waiting for Chrome to start...');
|
|
341
|
+
|
|
342
|
+
// Detect Chrome's bind address (IPv4 or IPv6)
|
|
343
|
+
let chromeBindAddress = null;
|
|
344
|
+
for (let i = 0; i < 10; i++) {
|
|
345
|
+
try {
|
|
346
|
+
execSync('powershell.exe -NoProfile -Command "Start-Sleep -Milliseconds 500"', { timeout: 2000 });
|
|
347
|
+
} catch { /* ignore */ }
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const netstatOutput = execSync(`netstat.exe -ano`, { encoding: 'utf8', timeout: 5000 });
|
|
351
|
+
const lines = netstatOutput.split('\n').filter(l => l.includes(':' + port) && l.includes('LISTEN'));
|
|
352
|
+
for (const line of lines) {
|
|
353
|
+
if (line.includes('127.0.0.1:' + port)) {
|
|
354
|
+
chromeBindAddress = '127.0.0.1';
|
|
355
|
+
break;
|
|
356
|
+
} else if (line.includes('[::1]:' + port)) {
|
|
357
|
+
chromeBindAddress = '::1';
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (chromeBindAddress) break;
|
|
362
|
+
} catch { /* ignore */ }
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (!chromeBindAddress) {
|
|
366
|
+
chromeBindAddress = '127.0.0.1';
|
|
367
|
+
log.dim('Could not detect Chrome bind address, assuming 127.0.0.1');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Set up port proxy - use v4tov6 for IPv6, v4tov4 for IPv4
|
|
371
|
+
const isIPv6 = chromeBindAddress === '::1';
|
|
372
|
+
const proxyType = isIPv6 ? 'v4tov6' : 'v4tov4';
|
|
373
|
+
const connectAddress = isIPv6 ? '::1' : '127.0.0.1';
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
execSync(
|
|
377
|
+
`powershell.exe -NoProfile -Command "netsh interface portproxy delete v4tov4 listenport=${port} listenaddress=0.0.0.0 2>\\$null; netsh interface portproxy delete v4tov6 listenport=${port} listenaddress=0.0.0.0 2>\\$null"`,
|
|
378
|
+
{ encoding: 'utf8', timeout: 5000 }
|
|
379
|
+
);
|
|
380
|
+
} catch { /* ignore */ }
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
execSync(
|
|
384
|
+
`powershell.exe -NoProfile -Command "netsh interface portproxy add ${proxyType} listenport=${port} listenaddress=0.0.0.0 connectport=${port} connectaddress=${connectAddress}"`,
|
|
385
|
+
{ encoding: 'utf8', timeout: 5000 }
|
|
386
|
+
);
|
|
387
|
+
log.success(`Port proxy configured: 0.0.0.0:${port} → ${connectAddress}:${port} (${proxyType})`);
|
|
388
|
+
} catch (e) {
|
|
389
|
+
log.warn(`Could not set up port proxy (need admin). Run manually:`);
|
|
390
|
+
console.log(` ${C.cyan}netsh interface portproxy add ${proxyType} listenport=${port} listenaddress=0.0.0.0 connectport=${port} connectaddress=${connectAddress}${C.reset}`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return true;
|
|
394
|
+
} catch (e) {
|
|
395
|
+
log.error(`Failed to start Chrome: ${e.message}`);
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Kill only Chrome processes that have "browsermonitor" in their profile path.
|
|
402
|
+
* This is safe to call - it will NEVER kill the user's regular Chrome browser.
|
|
403
|
+
*
|
|
404
|
+
* @param {boolean} usePowerShell - Use PowerShell instead of wmic (for calling from WSL)
|
|
405
|
+
* @returns {number} Number of Chrome processes killed
|
|
406
|
+
*/
|
|
407
|
+
export function killPuppeteerMonitorChromes(usePowerShell = false) {
|
|
408
|
+
try {
|
|
409
|
+
if (usePowerShell) {
|
|
410
|
+
const psScript = `$chromes = Get-WmiObject Win32_Process -Filter 'name=''chrome.exe''' | Select-Object ProcessId, CommandLine; $killed = 0; foreach ($chrome in $chromes) { if ($chrome.CommandLine -match 'browsermonitor') { Stop-Process -Id $chrome.ProcessId -Force -ErrorAction SilentlyContinue; $killed++; break } }; Write-Output $killed`;
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
const result = execFileSync(
|
|
414
|
+
'powershell.exe',
|
|
415
|
+
['-NoProfile', '-Command', psScript],
|
|
416
|
+
{ encoding: 'utf8', timeout: 15000 }
|
|
417
|
+
);
|
|
418
|
+
const killed = parseInt(result.trim(), 10) || 0;
|
|
419
|
+
if (killed > 0) {
|
|
420
|
+
log.success('Killed browsermonitor Chrome (PowerShell)');
|
|
421
|
+
}
|
|
422
|
+
return killed;
|
|
423
|
+
} catch (e) {
|
|
424
|
+
log.dim(`PowerShell kill failed: ${e.message}`);
|
|
425
|
+
return 0;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// wmic version - faster, used in most cases
|
|
430
|
+
const wmicOutput = execSync(
|
|
431
|
+
'wmic.exe process where "name=\'chrome.exe\'" get processid,commandline 2>/dev/null',
|
|
432
|
+
{ encoding: 'utf8', timeout: 10000 }
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
const lines = wmicOutput.split('\n').filter(line => line.trim());
|
|
436
|
+
const puppeteerMonitorPids = [];
|
|
437
|
+
|
|
438
|
+
for (const line of lines) {
|
|
439
|
+
if (line.includes('CommandLine') && line.includes('ProcessId')) continue;
|
|
440
|
+
|
|
441
|
+
if (line.includes('browsermonitor')) {
|
|
442
|
+
const pidMatch = line.match(/(\d+)\s*$/);
|
|
443
|
+
if (pidMatch) {
|
|
444
|
+
puppeteerMonitorPids.push(pidMatch[1]);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (puppeteerMonitorPids.length === 0) {
|
|
450
|
+
return 0;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const mainPid = puppeteerMonitorPids[0];
|
|
454
|
+
log.warn(`Found ${puppeteerMonitorPids.length} Chrome process(es) with browsermonitor profile`);
|
|
455
|
+
log.info(`Killing Chrome process tree (PID: ${mainPid})...`);
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
execSync(
|
|
459
|
+
`taskkill.exe /PID ${mainPid} /T /F 2>/dev/null`,
|
|
460
|
+
{ encoding: 'utf8', timeout: 10000 }
|
|
461
|
+
);
|
|
462
|
+
log.success('Killed existing browsermonitor Chrome');
|
|
463
|
+
execSync('sleep 1', { encoding: 'utf8' });
|
|
464
|
+
return 1;
|
|
465
|
+
} catch (e) {
|
|
466
|
+
log.dim(`taskkill returned: ${e.message}`);
|
|
467
|
+
return 0;
|
|
468
|
+
}
|
|
469
|
+
} catch (e) {
|
|
470
|
+
log.dim(`Could not check for existing Chrome: ${e.message}`);
|
|
471
|
+
return 0;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Legacy wrapper for backward compatibility.
|
|
477
|
+
*/
|
|
478
|
+
export function checkChromeRunning() {
|
|
479
|
+
const { instances, chromeRunning } = scanChromeInstances();
|
|
480
|
+
if (!chromeRunning) {
|
|
481
|
+
return { running: false, withDebugPort: false, debugPort: null };
|
|
482
|
+
}
|
|
483
|
+
if (instances.length === 0) {
|
|
484
|
+
return { running: true, withDebugPort: false, debugPort: null };
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
running: true,
|
|
488
|
+
withDebugPort: true,
|
|
489
|
+
debugPort: instances[0].port,
|
|
490
|
+
instances,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Legacy wrapper for backward compatibility.
|
|
496
|
+
*/
|
|
497
|
+
export function launchChromeFromWSL(chromePath, port) {
|
|
498
|
+
const distroName = getWslDistroName();
|
|
499
|
+
const wslPath = process.cwd();
|
|
500
|
+
const winPath = wslPath.replace(/\//g, '\\');
|
|
501
|
+
const profileDir = `\\\\wsl$\\${distroName}${winPath}\\.browsermonitor-profile`;
|
|
502
|
+
return startChromeOnWindows(chromePath, port, profileDir);
|
|
503
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WSL environment detection utilities.
|
|
3
|
+
*
|
|
4
|
+
* WSL2 runs in a lightweight Hyper-V VM with its own network namespace.
|
|
5
|
+
* 'localhost' in WSL refers to WSL itself, not Windows.
|
|
6
|
+
* To reach Windows services (like Chrome with remote debugging),
|
|
7
|
+
* we need the Windows host IP which is the default gateway from WSL's perspective.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import { execSync } from 'child_process';
|
|
12
|
+
import { C, log } from '../../utils/colors.mjs';
|
|
13
|
+
import { isWsl as isWslFromEnv } from '../../utils/env.mjs';
|
|
14
|
+
|
|
15
|
+
/** Re-export from single source of truth (utils/env.mjs). */
|
|
16
|
+
export const isWsl = isWslFromEnv;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get WSL distro name.
|
|
20
|
+
* @returns {string} Distro name (e.g., 'Ubuntu', 'OracleLinux_8_7')
|
|
21
|
+
*/
|
|
22
|
+
export function getWslDistroName() {
|
|
23
|
+
return process.env.WSL_DISTRO_NAME || 'Ubuntu';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Detect Windows host IP for WSL.
|
|
28
|
+
*
|
|
29
|
+
* The gateway IP is obtained from 'ip route' (e.g., 172.29.96.1).
|
|
30
|
+
* This is more reliable than /etc/resolv.conf nameserver because
|
|
31
|
+
* Windows port proxy (netsh portproxy) listens on the gateway IP.
|
|
32
|
+
*
|
|
33
|
+
* @param {{ quiet?: boolean }} [opts] - If quiet=true, don't log (caller will show compact block)
|
|
34
|
+
* @returns {string} Windows host IP or 'localhost' if not in WSL
|
|
35
|
+
*/
|
|
36
|
+
export function getWindowsHostForWSL(opts = {}) {
|
|
37
|
+
try {
|
|
38
|
+
const release = fs.readFileSync('/proc/version', 'utf8').toLowerCase();
|
|
39
|
+
if (!release.includes('microsoft') && !release.includes('wsl')) {
|
|
40
|
+
return 'localhost';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const route = execSync('ip route | grep default', { encoding: 'utf8' });
|
|
44
|
+
const match = route.match(/default via (\d+\.\d+\.\d+\.\d+)/);
|
|
45
|
+
if (match) {
|
|
46
|
+
if (!opts.quiet) {
|
|
47
|
+
log.info(`WSL detected, using Windows host IP: ${C.brightGreen}${match[1]}${C.reset}`);
|
|
48
|
+
}
|
|
49
|
+
return match[1];
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// Ignore errors
|
|
53
|
+
}
|
|
54
|
+
return 'localhost';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Convert WSL path to Windows UNC path.
|
|
59
|
+
* Note: UNC paths don't work well with Chrome profiles due to singleton detection.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} wslPath - WSL path (e.g., /srv/project)
|
|
62
|
+
* @returns {string} Windows UNC path (e.g., \\wsl$\Ubuntu\srv\project)
|
|
63
|
+
*/
|
|
64
|
+
export function wslToWindowsPath(wslPath) {
|
|
65
|
+
const distroName = getWslDistroName();
|
|
66
|
+
const winPath = wslPath.replace(/\//g, '\\');
|
|
67
|
+
return `\\\\wsl$\\${distroName}${winPath}`;
|
|
68
|
+
}
|