@monoes/monobrowse 1.0.6 → 1.0.7
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/README.md +68 -0
- package/dist/src/__tests__/adapters.test.d.ts +2 -0
- package/dist/src/__tests__/adapters.test.d.ts.map +1 -0
- package/dist/src/__tests__/adapters.test.js +104 -0
- package/dist/src/__tests__/adapters.test.js.map +1 -0
- package/dist/src/__tests__/batch-eval-parsing.test.d.ts +0 -0
- package/dist/src/__tests__/batch-eval-parsing.test.d.ts.map +0 -0
- package/dist/src/__tests__/batch-eval-parsing.test.js +0 -0
- package/dist/src/__tests__/batch-eval-parsing.test.js.map +0 -0
- package/dist/src/__tests__/box-model.test.d.ts +0 -0
- package/dist/src/__tests__/box-model.test.d.ts.map +0 -0
- package/dist/src/__tests__/box-model.test.js +0 -0
- package/dist/src/__tests__/box-model.test.js.map +0 -0
- package/dist/src/__tests__/browser-launch.test.d.ts +0 -0
- package/dist/src/__tests__/browser-launch.test.d.ts.map +0 -0
- package/dist/src/__tests__/browser-launch.test.js +0 -0
- package/dist/src/__tests__/browser-launch.test.js.map +0 -0
- package/dist/src/__tests__/cdp-client.test.d.ts +2 -0
- package/dist/src/__tests__/cdp-client.test.d.ts.map +1 -0
- package/dist/src/__tests__/cdp-client.test.js +326 -0
- package/dist/src/__tests__/cdp-client.test.js.map +1 -0
- package/dist/src/__tests__/close-browser.test.d.ts +2 -0
- package/dist/src/__tests__/close-browser.test.d.ts.map +1 -0
- package/dist/src/__tests__/close-browser.test.js +128 -0
- package/dist/src/__tests__/close-browser.test.js.map +1 -0
- package/dist/src/__tests__/console-log.test.d.ts +2 -0
- package/dist/src/__tests__/console-log.test.d.ts.map +1 -0
- package/dist/src/__tests__/console-log.test.js +195 -0
- package/dist/src/__tests__/console-log.test.js.map +1 -0
- package/dist/src/__tests__/find.test.d.ts +2 -0
- package/dist/src/__tests__/find.test.d.ts.map +1 -0
- package/dist/src/__tests__/find.test.js +299 -0
- package/dist/src/__tests__/find.test.js.map +1 -0
- package/dist/src/__tests__/frame-command.test.d.ts +2 -0
- package/dist/src/__tests__/frame-command.test.d.ts.map +1 -0
- package/dist/src/__tests__/frame-command.test.js +85 -0
- package/dist/src/__tests__/frame-command.test.js.map +1 -0
- package/dist/src/__tests__/output.test.d.ts +0 -0
- package/dist/src/__tests__/output.test.d.ts.map +0 -0
- package/dist/src/__tests__/output.test.js +0 -0
- package/dist/src/__tests__/output.test.js.map +0 -0
- package/dist/src/__tests__/ref-cache.test.d.ts +0 -0
- package/dist/src/__tests__/ref-cache.test.d.ts.map +0 -0
- package/dist/src/__tests__/ref-cache.test.js +43 -2
- package/dist/src/__tests__/ref-cache.test.js.map +1 -1
- package/dist/src/__tests__/session-state.test.d.ts +2 -0
- package/dist/src/__tests__/session-state.test.d.ts.map +1 -0
- package/dist/src/__tests__/session-state.test.js +203 -0
- package/dist/src/__tests__/session-state.test.js.map +1 -0
- package/dist/src/__tests__/snapshot.test.d.ts +2 -0
- package/dist/src/__tests__/snapshot.test.d.ts.map +1 -0
- package/dist/src/__tests__/snapshot.test.js +262 -0
- package/dist/src/__tests__/snapshot.test.js.map +1 -0
- package/dist/src/__tests__/storage-eval.test.d.ts +2 -0
- package/dist/src/__tests__/storage-eval.test.d.ts.map +1 -0
- package/dist/src/__tests__/storage-eval.test.js +133 -0
- package/dist/src/__tests__/storage-eval.test.js.map +1 -0
- package/dist/src/__tests__/tabs.test.d.ts +2 -0
- package/dist/src/__tests__/tabs.test.d.ts.map +1 -0
- package/dist/src/__tests__/tabs.test.js +161 -0
- package/dist/src/__tests__/tabs.test.js.map +1 -0
- package/dist/src/browser/action-builder/analyzer.d.ts +0 -0
- package/dist/src/browser/action-builder/analyzer.d.ts.map +0 -0
- package/dist/src/browser/action-builder/analyzer.js +0 -0
- package/dist/src/browser/action-builder/analyzer.js.map +0 -0
- package/dist/src/browser/action-builder/types.d.ts +0 -0
- package/dist/src/browser/action-builder/types.d.ts.map +0 -0
- package/dist/src/browser/action-builder/types.js +0 -0
- package/dist/src/browser/action-builder/types.js.map +0 -0
- package/dist/src/browser/actions.d.ts +0 -0
- package/dist/src/browser/actions.d.ts.map +0 -0
- package/dist/src/browser/actions.js +0 -0
- package/dist/src/browser/actions.js.map +0 -0
- package/dist/src/browser/adapters/gemini.d.ts +0 -0
- package/dist/src/browser/adapters/gemini.d.ts.map +0 -0
- package/dist/src/browser/adapters/gemini.js +0 -0
- package/dist/src/browser/adapters/gemini.js.map +0 -0
- package/dist/src/browser/adapters/google.d.ts +0 -0
- package/dist/src/browser/adapters/google.d.ts.map +0 -0
- package/dist/src/browser/adapters/google.js +0 -0
- package/dist/src/browser/adapters/google.js.map +0 -0
- package/dist/src/browser/adapters/index.d.ts +0 -0
- package/dist/src/browser/adapters/index.d.ts.map +0 -0
- package/dist/src/browser/adapters/index.js +0 -0
- package/dist/src/browser/adapters/index.js.map +0 -0
- package/dist/src/browser/adapters/instagram.d.ts +0 -0
- package/dist/src/browser/adapters/instagram.d.ts.map +0 -0
- package/dist/src/browser/adapters/instagram.js +0 -0
- package/dist/src/browser/adapters/instagram.js.map +0 -0
- package/dist/src/browser/adapters/linkedin.d.ts +0 -0
- package/dist/src/browser/adapters/linkedin.d.ts.map +0 -0
- package/dist/src/browser/adapters/linkedin.js +0 -0
- package/dist/src/browser/adapters/linkedin.js.map +0 -0
- package/dist/src/browser/adapters/microsoft.d.ts +0 -0
- package/dist/src/browser/adapters/microsoft.d.ts.map +0 -0
- package/dist/src/browser/adapters/microsoft.js +0 -0
- package/dist/src/browser/adapters/microsoft.js.map +0 -0
- package/dist/src/browser/adapters/x.d.ts +0 -0
- package/dist/src/browser/adapters/x.d.ts.map +0 -0
- package/dist/src/browser/adapters/x.js +0 -0
- package/dist/src/browser/adapters/x.js.map +0 -0
- package/dist/src/browser/batch.d.ts +0 -0
- package/dist/src/browser/batch.d.ts.map +0 -0
- package/dist/src/browser/batch.js +0 -0
- package/dist/src/browser/batch.js.map +0 -0
- package/dist/src/browser/browser.d.ts +4 -0
- package/dist/src/browser/browser.d.ts.map +1 -1
- package/dist/src/browser/browser.js +55 -7
- package/dist/src/browser/browser.js.map +1 -1
- package/dist/src/browser/cdp.d.ts +4 -1
- package/dist/src/browser/cdp.d.ts.map +1 -1
- package/dist/src/browser/cdp.js +45 -16
- package/dist/src/browser/cdp.js.map +1 -1
- package/dist/src/browser/console-log.d.ts +0 -0
- package/dist/src/browser/console-log.d.ts.map +0 -0
- package/dist/src/browser/console-log.js +0 -0
- package/dist/src/browser/console-log.js.map +0 -0
- package/dist/src/browser/dashboard/api-types.d.ts +0 -0
- package/dist/src/browser/dashboard/api-types.d.ts.map +0 -0
- package/dist/src/browser/dashboard/api-types.js +0 -0
- package/dist/src/browser/dashboard/api-types.js.map +0 -0
- package/dist/src/browser/dashboard/server.d.ts +0 -0
- package/dist/src/browser/dashboard/server.d.ts.map +0 -0
- package/dist/src/browser/dashboard/server.js +0 -0
- package/dist/src/browser/dashboard/server.js.map +0 -0
- package/dist/src/browser/dialog.d.ts +0 -0
- package/dist/src/browser/dialog.d.ts.map +0 -0
- package/dist/src/browser/dialog.js +0 -0
- package/dist/src/browser/dialog.js.map +0 -0
- package/dist/src/browser/emulation.d.ts +0 -0
- package/dist/src/browser/emulation.d.ts.map +0 -0
- package/dist/src/browser/emulation.js +0 -0
- package/dist/src/browser/emulation.js.map +0 -0
- package/dist/src/browser/find.d.ts +0 -0
- package/dist/src/browser/find.d.ts.map +0 -0
- package/dist/src/browser/find.js +0 -0
- package/dist/src/browser/find.js.map +0 -0
- package/dist/src/browser/har.d.ts +0 -0
- package/dist/src/browser/har.d.ts.map +0 -0
- package/dist/src/browser/har.js +0 -0
- package/dist/src/browser/har.js.map +0 -0
- package/dist/src/browser/index.d.ts +0 -0
- package/dist/src/browser/index.d.ts.map +0 -0
- package/dist/src/browser/index.js +0 -0
- package/dist/src/browser/index.js.map +0 -0
- package/dist/src/browser/network.d.ts +0 -0
- package/dist/src/browser/network.d.ts.map +0 -0
- package/dist/src/browser/network.js +0 -0
- package/dist/src/browser/network.js.map +0 -0
- package/dist/src/browser/pdf.d.ts +0 -0
- package/dist/src/browser/pdf.d.ts.map +0 -0
- package/dist/src/browser/pdf.js +0 -0
- package/dist/src/browser/pdf.js.map +0 -0
- package/dist/src/browser/profiler.d.ts +0 -0
- package/dist/src/browser/profiler.d.ts.map +0 -0
- package/dist/src/browser/profiler.js +0 -0
- package/dist/src/browser/profiler.js.map +0 -0
- package/dist/src/browser/record.d.ts +0 -0
- package/dist/src/browser/record.d.ts.map +0 -0
- package/dist/src/browser/record.js +0 -0
- package/dist/src/browser/record.js.map +0 -0
- package/dist/src/browser/ref-cache.d.ts +6 -1
- package/dist/src/browser/ref-cache.d.ts.map +1 -1
- package/dist/src/browser/ref-cache.js +21 -3
- package/dist/src/browser/ref-cache.js.map +1 -1
- package/dist/src/browser/screenshot.d.ts +0 -0
- package/dist/src/browser/screenshot.d.ts.map +0 -0
- package/dist/src/browser/screenshot.js +0 -0
- package/dist/src/browser/screenshot.js.map +0 -0
- package/dist/src/browser/session.d.ts +0 -0
- package/dist/src/browser/session.d.ts.map +0 -0
- package/dist/src/browser/session.js +0 -0
- package/dist/src/browser/session.js.map +0 -0
- package/dist/src/browser/snapshot.d.ts +0 -0
- package/dist/src/browser/snapshot.d.ts.map +0 -0
- package/dist/src/browser/snapshot.js +0 -0
- package/dist/src/browser/snapshot.js.map +0 -0
- package/dist/src/browser/storage.d.ts +0 -0
- package/dist/src/browser/storage.d.ts.map +0 -0
- package/dist/src/browser/storage.js +0 -0
- package/dist/src/browser/storage.js.map +0 -0
- package/dist/src/browser/tabs.d.ts +5 -1
- package/dist/src/browser/tabs.d.ts.map +1 -1
- package/dist/src/browser/tabs.js +4 -3
- package/dist/src/browser/tabs.js.map +1 -1
- package/dist/src/browser/trace.d.ts +0 -0
- package/dist/src/browser/trace.d.ts.map +0 -0
- package/dist/src/browser/trace.js +0 -0
- package/dist/src/browser/trace.js.map +0 -0
- package/dist/src/browser/types.d.ts +4 -0
- package/dist/src/browser/types.d.ts.map +1 -1
- package/dist/src/browser/types.js +0 -0
- package/dist/src/browser/types.js.map +1 -1
- package/dist/src/browser/vitals.d.ts +0 -0
- package/dist/src/browser/vitals.d.ts.map +0 -0
- package/dist/src/browser/vitals.js +0 -0
- package/dist/src/browser/vitals.js.map +0 -0
- package/dist/src/browser/wait.d.ts +0 -0
- package/dist/src/browser/wait.d.ts.map +0 -0
- package/dist/src/browser/wait.js +0 -0
- package/dist/src/browser/wait.js.map +0 -0
- package/dist/src/cli/action.d.ts +0 -0
- package/dist/src/cli/action.d.ts.map +0 -0
- package/dist/src/cli/action.js +0 -0
- package/dist/src/cli/action.js.map +0 -0
- package/dist/src/cli/commands.d.ts +0 -0
- package/dist/src/cli/commands.d.ts.map +1 -1
- package/dist/src/cli/commands.js +96 -4
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/cli/output.d.ts +0 -0
- package/dist/src/cli/output.d.ts.map +0 -0
- package/dist/src/cli/output.js +0 -0
- package/dist/src/cli/output.js.map +0 -0
- package/dist/src/cli/platform.d.ts +0 -0
- package/dist/src/cli/platform.d.ts.map +0 -0
- package/dist/src/cli/platform.js +0 -0
- package/dist/src/cli/platform.js.map +0 -0
- package/dist/src/cli/types.d.ts +0 -0
- package/dist/src/cli/types.d.ts.map +0 -0
- package/dist/src/cli/types.js +0 -0
- package/dist/src/cli/types.js.map +0 -0
- package/dist/src/cli.d.ts +0 -0
- package/dist/src/cli.d.ts.map +0 -0
- package/dist/src/cli.js +0 -0
- package/dist/src/cli.js.map +0 -0
- package/dist/src/index.d.ts +0 -0
- package/dist/src/index.d.ts.map +0 -0
- package/dist/src/index.js +0 -0
- package/dist/src/index.js.map +0 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -5
- package/src/__tests__/adapters.test.ts +125 -0
- package/src/__tests__/cdp-client.test.ts +375 -0
- package/src/__tests__/close-browser.test.ts +141 -0
- package/src/__tests__/console-log.test.ts +250 -0
- package/src/__tests__/find.test.ts +354 -0
- package/src/__tests__/frame-command.test.ts +98 -0
- package/src/__tests__/ref-cache.test.ts +47 -2
- package/src/__tests__/session-state.test.ts +251 -0
- package/src/__tests__/snapshot.test.ts +331 -0
- package/src/__tests__/storage-eval.test.ts +171 -0
- package/src/__tests__/tabs.test.ts +183 -0
- package/src/browser/browser.ts +59 -7
- package/src/browser/cdp.ts +52 -15
- package/src/browser/ref-cache.ts +29 -6
- package/src/browser/tabs.ts +12 -4
- package/src/browser/types.ts +4 -0
- package/src/cli/commands.ts +90 -4
package/src/browser/browser.ts
CHANGED
|
@@ -8,11 +8,15 @@ import type { BrowserConfig, CdpTarget } from './types.js';
|
|
|
8
8
|
import { CHROME_EXECUTABLES } from './types.js';
|
|
9
9
|
import { enableConsoleCapture, setupConsoleCapture } from './console-log.js';
|
|
10
10
|
import { setupDialogAutoHandling } from './dialog.js';
|
|
11
|
+
import { loadActivePortInfo } from './ref-cache.js';
|
|
11
12
|
|
|
12
13
|
const DEFAULT_PORT = 9222;
|
|
13
14
|
const LAUNCH_TIMEOUT = 10_000;
|
|
14
15
|
const POLL_INTERVAL = 200;
|
|
15
16
|
const BROWSER_CLOSE_TIMEOUT_MS = 3000;
|
|
17
|
+
// A cross-process persisted PID older than this is not trusted for the
|
|
18
|
+
// SIGKILL fallback in closeBrowser() — see the call site for why.
|
|
19
|
+
const PERSISTED_PID_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
16
20
|
|
|
17
21
|
// Tracks the PID of Chrome instances *we* spawned, keyed by CDP port, so
|
|
18
22
|
// closeBrowser() has a kill fallback when the graceful `Browser.close` CDP
|
|
@@ -20,6 +24,19 @@ const BROWSER_CLOSE_TIMEOUT_MS = 3000;
|
|
|
20
24
|
// to (already-running browser) are never recorded here — we only ever kill
|
|
21
25
|
// processes we launched ourselves.
|
|
22
26
|
const launchedPids = new Map<number, number>();
|
|
27
|
+
// userDataDir per launched port — informational, persisted alongside the pid
|
|
28
|
+
// so a later process could clean up the temp profile dir too if ever needed.
|
|
29
|
+
const launchedUserDataDirs = new Map<number, string>();
|
|
30
|
+
|
|
31
|
+
/** PID of the Chrome instance *this process* launched on `port`, if any. */
|
|
32
|
+
export function getLaunchedPid(port: number): number | undefined {
|
|
33
|
+
return launchedPids.get(port);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** userDataDir of the Chrome instance *this process* launched on `port`, if any. */
|
|
37
|
+
export function getLaunchedUserDataDir(port: number): string | undefined {
|
|
38
|
+
return launchedUserDataDirs.get(port);
|
|
39
|
+
}
|
|
23
40
|
|
|
24
41
|
function findChrome(executablePath?: string): string {
|
|
25
42
|
if (executablePath) {
|
|
@@ -185,9 +202,13 @@ async function launchOnFreePort(config: BrowserConfig, port: number): Promise<nu
|
|
|
185
202
|
stdio: 'ignore',
|
|
186
203
|
});
|
|
187
204
|
child.unref();
|
|
188
|
-
if (child.pid)
|
|
205
|
+
if (child.pid) {
|
|
206
|
+
launchedPids.set(port, child.pid);
|
|
207
|
+
launchedUserDataDirs.set(port, userDataDir);
|
|
208
|
+
}
|
|
189
209
|
|
|
190
|
-
const
|
|
210
|
+
const launchTimeout = config.launchTimeoutMs ?? LAUNCH_TIMEOUT;
|
|
211
|
+
const deadline = Date.now() + launchTimeout;
|
|
191
212
|
while (Date.now() < deadline) {
|
|
192
213
|
await sleep(POLL_INTERVAL);
|
|
193
214
|
if (await isPortOpen(port)) {
|
|
@@ -204,12 +225,12 @@ async function launchOnFreePort(config: BrowserConfig, port: number): Promise<nu
|
|
|
204
225
|
// squatting the port" (confusing generic timeout otherwise) via a raw TCP probe.
|
|
205
226
|
if (await isTcpPortOpen(port)) {
|
|
206
227
|
throw new Error(
|
|
207
|
-
`Port ${port} is occupied by a non-Chrome process (TCP connection succeeds but no CDP response within ${
|
|
228
|
+
`Port ${port} is occupied by a non-Chrome process (TCP connection succeeds but no CDP response within ${launchTimeout}ms). ` +
|
|
208
229
|
`Free the port or pass a different one.`
|
|
209
230
|
);
|
|
210
231
|
}
|
|
211
232
|
|
|
212
|
-
throw new Error(`Chrome failed to start on port ${port} within ${
|
|
233
|
+
throw new Error(`Chrome failed to start on port ${port} within ${launchTimeout}ms`);
|
|
213
234
|
}
|
|
214
235
|
|
|
215
236
|
export async function enableSessionDomains(client: CdpClient, sessionId: string): Promise<void> {
|
|
@@ -277,12 +298,43 @@ export async function closeBrowser(client: CdpClient, port: number): Promise<voi
|
|
|
277
298
|
// fall through to PID-kill fallback below
|
|
278
299
|
}
|
|
279
300
|
|
|
280
|
-
|
|
281
|
-
if (pid === undefined)
|
|
301
|
+
let pid = launchedPids.get(port);
|
|
302
|
+
if (pid === undefined) {
|
|
303
|
+
// Fresh process (each CLI invocation is its own node process — see
|
|
304
|
+
// module header) — launchedPids is per-process and empty here even
|
|
305
|
+
// though a prior `open` in a DIFFERENT process launched this Chrome.
|
|
306
|
+
// Fall back to the PID that process persisted via saveActivePort().
|
|
307
|
+
try {
|
|
308
|
+
const persisted = await loadActivePortInfo();
|
|
309
|
+
if (persisted && persisted.port === port && persisted.launched && persisted.pid) {
|
|
310
|
+
// Bound how far we trust a cross-process PID: if the Chrome we
|
|
311
|
+
// launched died outside this flow (OOM-killed, host slept, etc.)
|
|
312
|
+
// without active-port.json ever being cleared, the OS can recycle
|
|
313
|
+
// that PID for an unrelated process. There is no cmdline/start-time
|
|
314
|
+
// check available portably, so a coarse age bound is the practical
|
|
315
|
+
// guard — a PID persisted more than a day ago is far more likely to
|
|
316
|
+
// be stale/reused than to still be the same live Chrome.
|
|
317
|
+
const age = persisted.savedAt !== undefined ? Date.now() - persisted.savedAt : undefined;
|
|
318
|
+
if (age === undefined || age <= PERSISTED_PID_MAX_AGE_MS) {
|
|
319
|
+
pid = persisted.pid;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} catch {
|
|
323
|
+
// best-effort — fall through to "nothing to kill" below
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (pid === undefined) return; // not a process we launched, or too stale to trust — nothing to kill
|
|
282
327
|
launchedPids.delete(port);
|
|
283
328
|
|
|
329
|
+
// Liveness-check before every SIGKILL (not just the safety-net path below)
|
|
330
|
+
// — process.kill(pid, 0) throws ESRCH if the PID is no longer running,
|
|
331
|
+
// which also catches the case where the PID has already been recycled by
|
|
332
|
+
// an unrelated process that happens to have exited too.
|
|
284
333
|
if (!gracefullyClosed) {
|
|
285
|
-
try {
|
|
334
|
+
try {
|
|
335
|
+
process.kill(pid, 0);
|
|
336
|
+
process.kill(pid, 'SIGKILL');
|
|
337
|
+
} catch { /* already exited, or never was — nothing to do */ }
|
|
286
338
|
return;
|
|
287
339
|
}
|
|
288
340
|
|
package/src/browser/cdp.ts
CHANGED
|
@@ -1,13 +1,41 @@
|
|
|
1
1
|
import { WebSocket } from 'ws';
|
|
2
2
|
import type { CdpCommand, CdpResponse, CdpTarget } from './types.js';
|
|
3
3
|
|
|
4
|
+
// send() had no timeout of its own — a command whose response never arrives
|
|
5
|
+
// (wedged/crashed renderer, dropped socket that never fires 'close') left a
|
|
6
|
+
// pendingCommands entry forever, hanging whatever awaited it (and, since
|
|
7
|
+
// most CLI actions await a single send(), the whole CLI process). Ad-hoc
|
|
8
|
+
// timeouts were previously patched in at a handful of call sites (e.g.
|
|
9
|
+
// actions.ts's evaluateJs) instead of here, so most commands had no
|
|
10
|
+
// protection at all. This is the default for every send() unless a caller
|
|
11
|
+
// passes 0 (no timeout) or a different value.
|
|
12
|
+
export const DEFAULT_CDP_SEND_TIMEOUT_MS = 30_000;
|
|
13
|
+
|
|
4
14
|
export class CdpClient {
|
|
5
15
|
private ws: WebSocket | null = null;
|
|
6
|
-
private pendingCommands = new Map<number, { resolve: (r: CdpResponse) => void; reject: (e: Error) => void }>();
|
|
16
|
+
private pendingCommands = new Map<number, { resolve: (r: CdpResponse) => void; reject: (e: Error) => void; timer?: ReturnType<typeof setTimeout> }>();
|
|
7
17
|
private eventListeners = new Map<string, Set<(params: Record<string, unknown>, sessionId?: string) => void>>();
|
|
8
18
|
private nextId = 1;
|
|
9
19
|
private connected = false;
|
|
10
20
|
|
|
21
|
+
constructor() {
|
|
22
|
+
// Target.targetCrashed/targetDestroyed fire over the same WS connection
|
|
23
|
+
// as command responses, but a crashed renderer's connection can otherwise
|
|
24
|
+
// sit open with no 'close'/'error' event ever firing — flush pending
|
|
25
|
+
// commands here so send() doesn't have to wait out its own timeout.
|
|
26
|
+
this.on('Target.targetCrashed', () => this.flushPending(new Error('CDP target crashed')));
|
|
27
|
+
this.on('Target.targetDestroyed', () => this.flushPending(new Error('CDP target destroyed')));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private flushPending(err: Error): void {
|
|
31
|
+
this.connected = false;
|
|
32
|
+
for (const { reject: r, timer } of this.pendingCommands.values()) {
|
|
33
|
+
if (timer) clearTimeout(timer);
|
|
34
|
+
r(err);
|
|
35
|
+
}
|
|
36
|
+
this.pendingCommands.clear();
|
|
37
|
+
}
|
|
38
|
+
|
|
11
39
|
async connect(wsUrl: string): Promise<void> {
|
|
12
40
|
return new Promise((resolve, reject) => {
|
|
13
41
|
this.ws = new WebSocket(wsUrl);
|
|
@@ -22,18 +50,12 @@ export class CdpClient {
|
|
|
22
50
|
reject(err);
|
|
23
51
|
} else {
|
|
24
52
|
// Post-connect: flush all pending commands — 'close' may not fire on all platforms
|
|
25
|
-
this.
|
|
26
|
-
for (const { reject: r } of this.pendingCommands.values()) r(err);
|
|
27
|
-
this.pendingCommands.clear();
|
|
53
|
+
this.flushPending(err instanceof Error ? err : new Error(String(err)));
|
|
28
54
|
}
|
|
29
55
|
});
|
|
30
56
|
|
|
31
57
|
this.ws.on('close', () => {
|
|
32
|
-
this.
|
|
33
|
-
for (const { reject: r } of this.pendingCommands.values()) {
|
|
34
|
-
r(new Error('CDP connection closed'));
|
|
35
|
-
}
|
|
36
|
-
this.pendingCommands.clear();
|
|
58
|
+
this.flushPending(new Error('CDP connection closed'));
|
|
37
59
|
});
|
|
38
60
|
|
|
39
61
|
this.ws.on('message', (data) => {
|
|
@@ -62,7 +84,12 @@ export class CdpClient {
|
|
|
62
84
|
});
|
|
63
85
|
}
|
|
64
86
|
|
|
65
|
-
send<T = Record<string, unknown>>(
|
|
87
|
+
send<T = Record<string, unknown>>(
|
|
88
|
+
method: string,
|
|
89
|
+
params?: Record<string, unknown>,
|
|
90
|
+
sessionId?: string,
|
|
91
|
+
timeoutMs: number = DEFAULT_CDP_SEND_TIMEOUT_MS
|
|
92
|
+
): Promise<T> {
|
|
66
93
|
return new Promise((resolve, reject) => {
|
|
67
94
|
if (!this.ws || !this.connected) {
|
|
68
95
|
reject(new Error('CDP not connected'));
|
|
@@ -76,12 +103,24 @@ export class CdpClient {
|
|
|
76
103
|
const id = this.nextId++;
|
|
77
104
|
const cmd: CdpCommand = { id, method, params };
|
|
78
105
|
if (sessionId) cmd.sessionId = sessionId;
|
|
106
|
+
|
|
107
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
108
|
+
if (timeoutMs > 0) {
|
|
109
|
+
timer = setTimeout(() => {
|
|
110
|
+
this.pendingCommands.delete(id);
|
|
111
|
+
reject(new Error(`CDP command "${method}" timed out after ${timeoutMs}ms`));
|
|
112
|
+
}, timeoutMs);
|
|
113
|
+
timer.unref?.();
|
|
114
|
+
}
|
|
115
|
+
|
|
79
116
|
this.pendingCommands.set(id, {
|
|
80
|
-
resolve: (r) => resolve((r.result ?? {}) as T),
|
|
81
|
-
reject,
|
|
117
|
+
resolve: (r) => { if (timer) clearTimeout(timer); resolve((r.result ?? {}) as T); },
|
|
118
|
+
reject: (e) => { if (timer) clearTimeout(timer); reject(e); },
|
|
119
|
+
timer,
|
|
82
120
|
});
|
|
83
121
|
this.ws.send(JSON.stringify(cmd), (err) => {
|
|
84
122
|
if (err) {
|
|
123
|
+
if (timer) clearTimeout(timer);
|
|
85
124
|
this.pendingCommands.delete(id);
|
|
86
125
|
reject(err);
|
|
87
126
|
}
|
|
@@ -120,9 +159,7 @@ export class CdpClient {
|
|
|
120
159
|
}
|
|
121
160
|
|
|
122
161
|
close(): void {
|
|
123
|
-
this.
|
|
124
|
-
for (const { reject: r } of this.pendingCommands.values()) r(new Error('CDP connection closed'));
|
|
125
|
-
this.pendingCommands.clear();
|
|
162
|
+
this.flushPending(new Error('CDP connection closed'));
|
|
126
163
|
this.ws?.close();
|
|
127
164
|
this.ws = null;
|
|
128
165
|
this.eventListeners.clear();
|
package/src/browser/ref-cache.ts
CHANGED
|
@@ -97,14 +97,29 @@ export async function clearRefCache(): Promise<void> {
|
|
|
97
97
|
* falling back to the hardcoded default port and launching/attaching to a
|
|
98
98
|
* second, unrelated Chrome instance.
|
|
99
99
|
*/
|
|
100
|
-
export async function saveActivePort(
|
|
100
|
+
export async function saveActivePort(
|
|
101
|
+
port: number,
|
|
102
|
+
opts?: { launched?: boolean; pid?: number; userDataDir?: string }
|
|
103
|
+
): Promise<void> {
|
|
101
104
|
try {
|
|
102
105
|
await mkdir(CACHE_DIR, { recursive: true });
|
|
103
106
|
// `launched` records provenance: true = monobrowse spawned this Chrome
|
|
104
107
|
// (safe to Browser.close later); false = attached to a browser someone
|
|
105
108
|
// else owns (must never be killed). Absent (old files) reads as launched
|
|
106
109
|
// — matches pre-flag behavior where every persisted port came from open.
|
|
107
|
-
|
|
110
|
+
//
|
|
111
|
+
// `pid`/`userDataDir` are persisted so a LATER, fresh CLI process (each
|
|
112
|
+
// command is its own node process — see module header) can still find
|
|
113
|
+
// and kill the Chrome this one launched. Without this, closeBrowser()'s
|
|
114
|
+
// PID-kill fallback can never fire outside the launching process, since
|
|
115
|
+
// the in-memory launchedPids Map (browser.ts) is empty there.
|
|
116
|
+
await writeFile(PORT_FILE, JSON.stringify({
|
|
117
|
+
port,
|
|
118
|
+
launched: opts?.launched !== false,
|
|
119
|
+
pid: opts?.pid,
|
|
120
|
+
userDataDir: opts?.userDataDir,
|
|
121
|
+
savedAt: Date.now(),
|
|
122
|
+
}));
|
|
108
123
|
} catch {
|
|
109
124
|
// Best-effort — persistence failure just means the next process falls
|
|
110
125
|
// back to the hardcoded default port, matching prior behavior.
|
|
@@ -126,13 +141,21 @@ export async function loadActivePort(): Promise<number | null> {
|
|
|
126
141
|
return (await loadActivePortInfo())?.port ?? null;
|
|
127
142
|
}
|
|
128
143
|
|
|
129
|
-
/** Load the persisted active port with its provenance flag. */
|
|
130
|
-
export async function loadActivePortInfo(): Promise<
|
|
144
|
+
/** Load the persisted active port with its provenance flag, PID, user-data-dir, and save timestamp. */
|
|
145
|
+
export async function loadActivePortInfo(): Promise<
|
|
146
|
+
{ port: number; launched: boolean; pid?: number; userDataDir?: string; savedAt?: number } | null
|
|
147
|
+
> {
|
|
131
148
|
try {
|
|
132
149
|
const raw = await readFile(PORT_FILE, 'utf8');
|
|
133
|
-
const data = JSON.parse(raw) as { port?: unknown; launched?: unknown };
|
|
150
|
+
const data = JSON.parse(raw) as { port?: unknown; launched?: unknown; pid?: unknown; userDataDir?: unknown; savedAt?: unknown };
|
|
134
151
|
if (typeof data.port === 'number' && Number.isInteger(data.port) && data.port >= 1024 && data.port <= 65535) {
|
|
135
|
-
return {
|
|
152
|
+
return {
|
|
153
|
+
port: data.port,
|
|
154
|
+
launched: data.launched !== false,
|
|
155
|
+
pid: typeof data.pid === 'number' && Number.isInteger(data.pid) && data.pid > 0 ? data.pid : undefined,
|
|
156
|
+
userDataDir: typeof data.userDataDir === 'string' ? data.userDataDir : undefined,
|
|
157
|
+
savedAt: typeof data.savedAt === 'number' ? data.savedAt : undefined,
|
|
158
|
+
};
|
|
136
159
|
}
|
|
137
160
|
return null;
|
|
138
161
|
} catch {
|
package/src/browser/tabs.ts
CHANGED
|
@@ -24,11 +24,16 @@ export async function activateTab(client: CdpClient, oldSessionId: string, targe
|
|
|
24
24
|
return result.sessionId;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
export interface FrameSwitchResult {
|
|
28
|
+
url: string | null;
|
|
29
|
+
sessionId: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
export async function switchToFrame(
|
|
28
33
|
client: CdpClient,
|
|
29
34
|
sessionId: string,
|
|
30
35
|
frameSelector: string
|
|
31
|
-
): Promise<
|
|
36
|
+
): Promise<FrameSwitchResult> {
|
|
32
37
|
const { result } = await client.send<{ result: { result: { value: string | null } } }>(
|
|
33
38
|
'Runtime.evaluate',
|
|
34
39
|
{
|
|
@@ -50,7 +55,7 @@ export async function switchToFrame(
|
|
|
50
55
|
const match = frameTree.childFrames?.find(
|
|
51
56
|
(cf) => cf.frame.url === frameSrc || cf.frame.id === frameSelector
|
|
52
57
|
);
|
|
53
|
-
if (!match) return frameSrc;
|
|
58
|
+
if (!match) return { url: frameSrc, sessionId: null };
|
|
54
59
|
|
|
55
60
|
const targets = await client.send<{ targetInfos: Array<{ targetId: string; type: string; url: string }> }>(
|
|
56
61
|
'Target.getTargets', {}
|
|
@@ -59,8 +64,11 @@ export async function switchToFrame(
|
|
|
59
64
|
(t) => t.type === 'iframe' && t.url === match.frame.url
|
|
60
65
|
);
|
|
61
66
|
if (oopif) {
|
|
62
|
-
await client.send
|
|
67
|
+
const attached = await client.send<{ sessionId: string }>(
|
|
68
|
+
'Target.attachToTarget', { targetId: oopif.targetId, flatten: true }
|
|
69
|
+
);
|
|
70
|
+
return { url: frameSrc, sessionId: attached.sessionId };
|
|
63
71
|
}
|
|
64
72
|
|
|
65
|
-
return frameSrc;
|
|
73
|
+
return { url: frameSrc, sessionId: null };
|
|
66
74
|
}
|
package/src/browser/types.ts
CHANGED
|
@@ -9,6 +9,10 @@ export interface BrowserConfig {
|
|
|
9
9
|
executablePath?: string;
|
|
10
10
|
userDataDir?: string;
|
|
11
11
|
args?: string[];
|
|
12
|
+
/** Milliseconds to wait for the launched Chrome to open its CDP port
|
|
13
|
+
* before giving up. Default 10000 — raise this for environments where
|
|
14
|
+
* process spawn is consistently slower (e.g. constrained CI runners). */
|
|
15
|
+
launchTimeoutMs?: number;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
export interface SessionState {
|
package/src/cli/commands.ts
CHANGED
|
@@ -16,11 +16,52 @@ let _sessionId = '';
|
|
|
16
16
|
let _targetId = '';
|
|
17
17
|
let _port = DEFAULT_PORT;
|
|
18
18
|
let _refs: Map<string, ElementRef> = new Map();
|
|
19
|
+
// Saved parent sessionId when inside an iframe — restored by `frame main`.
|
|
20
|
+
let _parentSessionId = '';
|
|
19
21
|
|
|
20
22
|
async function getBrowser() {
|
|
21
23
|
return import('../index.js');
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
// Best-effort cleanup on Ctrl-C / kill so a launched Chrome doesn't linger as
|
|
27
|
+
// an orphan process when a command is interrupted mid-flight (e.g. during a
|
|
28
|
+
// long-running wait/eval that CdpClient.send()'s own timeout hasn't tripped
|
|
29
|
+
// yet).
|
|
30
|
+
//
|
|
31
|
+
// This module is imported for EVERY `monomind` CLI invocation (commands/
|
|
32
|
+
// index.ts statically imports browse.ts, which imports this file) — not just
|
|
33
|
+
// browse commands — so the handler is only *registered* once a browser
|
|
34
|
+
// session is actually launched here (see ensureSignalCleanupHandlers(),
|
|
35
|
+
// called from the `open` action below), not at module load. Registering
|
|
36
|
+
// unconditionally at import time would call process.exit() on Ctrl-C for
|
|
37
|
+
// every unrelated monomind command (e.g. a long-running `org run` daemon),
|
|
38
|
+
// pre-empting whatever other SIGINT/SIGTERM handling that process needs.
|
|
39
|
+
const SIGNAL_EXIT_CODES: Record<'SIGINT' | 'SIGTERM', number> = { SIGINT: 130, SIGTERM: 143 };
|
|
40
|
+
let _signalHandlersRegistered = false;
|
|
41
|
+
function ensureSignalCleanupHandlers(): void {
|
|
42
|
+
if (_signalHandlersRegistered) return;
|
|
43
|
+
_signalHandlersRegistered = true;
|
|
44
|
+
for (const sig of ['SIGINT', 'SIGTERM'] as const) {
|
|
45
|
+
process.on(sig, () => {
|
|
46
|
+
void (async () => {
|
|
47
|
+
try {
|
|
48
|
+
const browser = await getBrowser();
|
|
49
|
+
if (_client) {
|
|
50
|
+
await browser.closeBrowser(_client, _port);
|
|
51
|
+
} else {
|
|
52
|
+
const pid = browser.getLaunchedPid(_port);
|
|
53
|
+
if (pid !== undefined) {
|
|
54
|
+
try { process.kill(pid, 'SIGKILL'); } catch { /* already exited */ }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// best-effort — never block process exit on cleanup failure
|
|
59
|
+
}
|
|
60
|
+
})().finally(() => process.exit(SIGNAL_EXIT_CODES[sig]));
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
24
65
|
// Each CLI invocation is a fresh process, so this module's `_port` variable
|
|
25
66
|
// is always freshly initialized to DEFAULT_PORT — a `--port 9333` passed to
|
|
26
67
|
// an earlier `open` command has no effect on this process's own default.
|
|
@@ -62,6 +103,7 @@ async function ensureConnected(port: number, targetId?: string) {
|
|
|
62
103
|
_client = conn.client;
|
|
63
104
|
_sessionId = conn.sessionId;
|
|
64
105
|
_targetId = conn.target.id;
|
|
106
|
+
_parentSessionId = '';
|
|
65
107
|
_refs = new Map();
|
|
66
108
|
await hydrateRefsFromCache(browser, _targetId, conn.target.url);
|
|
67
109
|
}
|
|
@@ -157,6 +199,7 @@ async function switchToHeaded(url: string, port: number): Promise<void> {
|
|
|
157
199
|
_client.close();
|
|
158
200
|
_client = null;
|
|
159
201
|
_sessionId = '';
|
|
202
|
+
_parentSessionId = '';
|
|
160
203
|
_targetId = '';
|
|
161
204
|
}
|
|
162
205
|
|
|
@@ -203,6 +246,7 @@ async function switchToHeaded(url: string, port: number): Promise<void> {
|
|
|
203
246
|
_client.close();
|
|
204
247
|
_client = null;
|
|
205
248
|
_sessionId = '';
|
|
249
|
+
_parentSessionId = '';
|
|
206
250
|
_targetId = '';
|
|
207
251
|
|
|
208
252
|
// Relaunch headless and restore session
|
|
@@ -277,14 +321,40 @@ const openCommand: Command = {
|
|
|
277
321
|
prevClient.close();
|
|
278
322
|
_client = null;
|
|
279
323
|
_sessionId = '';
|
|
324
|
+
_parentSessionId = '';
|
|
280
325
|
_targetId = '';
|
|
281
326
|
_refs = new Map();
|
|
282
327
|
}
|
|
283
328
|
|
|
284
329
|
_port = await browser.launchBrowser({ port, headless: !forceHeaded });
|
|
285
330
|
// Persist the active port so subsequent CLI invocations (each a fresh
|
|
286
|
-
// process) default to attaching here instead of hardcoded 9222.
|
|
287
|
-
|
|
331
|
+
// process) default to attaching here instead of hardcoded 9222. Also
|
|
332
|
+
// persist the launched PID/userDataDir so a later process's closeBrowser
|
|
333
|
+
// can still kill this Chrome even though launchedPids (browser.ts) is
|
|
334
|
+
// per-process and empty there.
|
|
335
|
+
//
|
|
336
|
+
// launchBrowser() can either LAUNCH a fresh Chrome or ATTACH to one
|
|
337
|
+
// already listening on the requested port (see its own "attach if
|
|
338
|
+
// already Chrome" comment) — it returns only a port number, with no
|
|
339
|
+
// signal telling this caller which happened. getLaunchedPid(_port) is
|
|
340
|
+
// undefined on the attach path (this process never spawned anything).
|
|
341
|
+
// Bug fixed here: unconditionally saving {pid: undefined, ...} on
|
|
342
|
+
// attach used to CLOBBER a real PID a previous `open` had already
|
|
343
|
+
// persisted for this exact port, destroying the only way a later
|
|
344
|
+
// process's closeBrowser() PID-kill fallback could ever find it.
|
|
345
|
+
const freshPid = browser.getLaunchedPid(_port);
|
|
346
|
+
const freshUserDataDir = browser.getLaunchedUserDataDir(_port);
|
|
347
|
+
if (freshPid !== undefined) {
|
|
348
|
+
await browser.saveActivePort(_port, { pid: freshPid, userDataDir: freshUserDataDir });
|
|
349
|
+
} else {
|
|
350
|
+
// Attach path: preserve whatever PID/userDataDir was already on file
|
|
351
|
+
// for this port (most likely from the process that originally
|
|
352
|
+
// launched it) instead of overwriting with undefined.
|
|
353
|
+
const existing = await browser.loadActivePortInfo();
|
|
354
|
+
const preserved = existing && existing.port === _port ? existing : undefined;
|
|
355
|
+
await browser.saveActivePort(_port, { pid: preserved?.pid, userDataDir: preserved?.userDataDir });
|
|
356
|
+
}
|
|
357
|
+
ensureSignalCleanupHandlers();
|
|
288
358
|
const conn = await browser.connectToTarget(_port);
|
|
289
359
|
_client = conn.client;
|
|
290
360
|
_sessionId = conn.sessionId;
|
|
@@ -1275,6 +1345,7 @@ const closeCommand: Command = {
|
|
|
1275
1345
|
client.close();
|
|
1276
1346
|
_client = null;
|
|
1277
1347
|
_sessionId = '';
|
|
1348
|
+
_parentSessionId = '';
|
|
1278
1349
|
_targetId = '';
|
|
1279
1350
|
_refs = new Map();
|
|
1280
1351
|
// Session is gone — forget the persisted port and stale refs so later
|
|
@@ -1919,10 +1990,23 @@ const frameCommand: Command = {
|
|
|
1919
1990
|
if (!target) throw new Error('Usage: monomind browse frame <selector>|main');
|
|
1920
1991
|
|
|
1921
1992
|
if (target === 'main') {
|
|
1993
|
+
if (_parentSessionId) {
|
|
1994
|
+
// Detach from the OOPIF session (best-effort — it may already be gone)
|
|
1995
|
+
await client.send('Target.detachFromTarget', { sessionId: _sessionId }).catch(() => {});
|
|
1996
|
+
_sessionId = _parentSessionId;
|
|
1997
|
+
_parentSessionId = '';
|
|
1998
|
+
}
|
|
1922
1999
|
output.printSuccess('Switched to main frame');
|
|
1923
2000
|
} else {
|
|
1924
|
-
const
|
|
1925
|
-
|
|
2001
|
+
const frameResult = await browser.switchToFrame(client, sessionId, target);
|
|
2002
|
+
if (frameResult.sessionId) {
|
|
2003
|
+
// Save the parent session so `frame main` can restore it
|
|
2004
|
+
_parentSessionId = _sessionId;
|
|
2005
|
+
_sessionId = frameResult.sessionId;
|
|
2006
|
+
// Enable CDP domains on the iframe's session so subsequent commands work
|
|
2007
|
+
await browser.enableSessionDomains(client, _sessionId);
|
|
2008
|
+
}
|
|
2009
|
+
output.printSuccess(`Switched to frame: ${frameResult.url ?? target}`);
|
|
1926
2010
|
}
|
|
1927
2011
|
return { success: true };
|
|
1928
2012
|
},
|
|
@@ -1974,6 +2058,7 @@ const tabCommand: Command = {
|
|
|
1974
2058
|
client.close();
|
|
1975
2059
|
_client = null;
|
|
1976
2060
|
_sessionId = '';
|
|
2061
|
+
_parentSessionId = '';
|
|
1977
2062
|
_targetId = '';
|
|
1978
2063
|
_refs = new Map();
|
|
1979
2064
|
} else {
|
|
@@ -2726,6 +2811,7 @@ const connectCommand: Command = {
|
|
|
2726
2811
|
prevClient.close();
|
|
2727
2812
|
_client = null;
|
|
2728
2813
|
_sessionId = '';
|
|
2814
|
+
_parentSessionId = '';
|
|
2729
2815
|
_targetId = '';
|
|
2730
2816
|
_refs = new Map();
|
|
2731
2817
|
}
|