@monoes/monobrowse 1.0.7 → 1.0.8
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 +191 -0
- package/README.md +0 -0
- package/dist/src/__tests__/close-browser.test.js +64 -4
- package/dist/src/__tests__/close-browser.test.js.map +1 -1
- package/dist/src/browser/browser.d.ts +9 -0
- package/dist/src/browser/browser.d.ts.map +1 -1
- package/dist/src/browser/browser.js +44 -9
- package/dist/src/browser/browser.js.map +1 -1
- package/dist/src/browser/dashboard/ui.html +0 -0
- package/dist/src/cli.js +0 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -10
- package/src/__tests__/adapters.test.ts +0 -0
- package/src/__tests__/batch-eval-parsing.test.ts +0 -0
- package/src/__tests__/box-model.test.ts +0 -0
- package/src/__tests__/browser-launch.test.ts +0 -0
- package/src/__tests__/cdp-client.test.ts +0 -0
- package/src/__tests__/close-browser.test.ts +68 -4
- package/src/__tests__/console-log.test.ts +0 -0
- package/src/__tests__/find.test.ts +0 -0
- package/src/__tests__/frame-command.test.ts +0 -0
- package/src/__tests__/output.test.ts +0 -0
- package/src/__tests__/ref-cache.test.ts +0 -0
- package/src/__tests__/session-state.test.ts +0 -0
- package/src/__tests__/snapshot.test.ts +0 -0
- package/src/__tests__/storage-eval.test.ts +0 -0
- package/src/__tests__/tabs.test.ts +0 -0
- package/src/browser/action-builder/analyzer.ts +0 -0
- package/src/browser/action-builder/types.ts +0 -0
- package/src/browser/actions.ts +0 -0
- package/src/browser/adapters/gemini.ts +0 -0
- package/src/browser/adapters/google.ts +0 -0
- package/src/browser/adapters/index.ts +0 -0
- package/src/browser/adapters/instagram.ts +0 -0
- package/src/browser/adapters/linkedin.ts +0 -0
- package/src/browser/adapters/microsoft.ts +0 -0
- package/src/browser/adapters/x.ts +0 -0
- package/src/browser/batch.ts +0 -0
- package/src/browser/browser.ts +42 -9
- package/src/browser/cdp.ts +0 -0
- package/src/browser/console-log.ts +0 -0
- package/src/browser/dashboard/api-types.ts +0 -0
- package/src/browser/dashboard/server.ts +0 -0
- package/src/browser/dashboard/ui.html +0 -0
- package/src/browser/dialog.ts +0 -0
- package/src/browser/emulation.ts +0 -0
- package/src/browser/find.ts +0 -0
- package/src/browser/har.ts +0 -0
- package/src/browser/index.ts +0 -0
- package/src/browser/network.ts +0 -0
- package/src/browser/pdf.ts +0 -0
- package/src/browser/profiler.ts +0 -0
- package/src/browser/record.ts +0 -0
- package/src/browser/ref-cache.ts +0 -0
- package/src/browser/screenshot.ts +0 -0
- package/src/browser/session.ts +0 -0
- package/src/browser/snapshot.ts +0 -0
- package/src/browser/storage.ts +0 -0
- package/src/browser/tabs.ts +0 -0
- package/src/browser/trace.ts +0 -0
- package/src/browser/vitals.ts +0 -0
- package/src/browser/wait.ts +0 -0
- package/src/cli/action.ts +0 -0
- package/src/cli/commands.ts +0 -0
- package/src/cli/output.ts +0 -0
- package/src/cli/platform.ts +0 -0
- package/src/cli/types.ts +0 -0
- package/src/cli.ts +0 -0
- package/src/index.ts +0 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoes/monobrowse",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Native browser automation via Chrome DevTools Protocol
|
|
3
|
+
"version": "1.0.8",
|
|
4
|
+
"description": "Native browser automation via Chrome DevTools Protocol — the engine powering monomind browse",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"monobrowse": "./dist/src/cli.js"
|
|
@@ -33,13 +33,6 @@
|
|
|
33
33
|
"engines": {
|
|
34
34
|
"node": ">=18.0.0"
|
|
35
35
|
},
|
|
36
|
-
"scripts": {
|
|
37
|
-
"build": "tsc && node ../../../scripts/build-fs.mjs copy-into dist/src/browser/dashboard src/browser/dashboard/ui.html",
|
|
38
|
-
"test": "vitest run",
|
|
39
|
-
"test:coverage": "vitest run --coverage",
|
|
40
|
-
"prebuild": "node ../../../scripts/build-fs.mjs clean dist tsconfig.tsbuildinfo",
|
|
41
|
-
"prepublishOnly": "npm run build"
|
|
42
|
-
},
|
|
43
36
|
"keywords": [
|
|
44
37
|
"browser-automation",
|
|
45
38
|
"cdp",
|
|
@@ -66,5 +59,11 @@
|
|
|
66
59
|
"devDependencies": {
|
|
67
60
|
"typescript": "^7.0.2",
|
|
68
61
|
"vitest": "^4.1.4"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "tsc && node ../../../scripts/build-fs.mjs copy-into dist/src/browser/dashboard src/browser/dashboard/ui.html",
|
|
65
|
+
"test": "vitest run",
|
|
66
|
+
"test:coverage": "vitest run --coverage",
|
|
67
|
+
"prebuild": "node ../../../scripts/build-fs.mjs clean dist tsconfig.tsbuildinfo"
|
|
69
68
|
}
|
|
70
|
-
}
|
|
69
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -30,10 +30,15 @@ class FakeWs {
|
|
|
30
30
|
emit(event: string, ...args: unknown[]): void {
|
|
31
31
|
for (const fn of [...(this.handlers.get(event) ?? [])]) fn(...args);
|
|
32
32
|
}
|
|
33
|
-
send(
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
/** When true, send() acknowledges the CDP command by delivering a matching
|
|
34
|
+
* empty result — the GRACEFUL path. Default false leaves it hanging so
|
|
35
|
+
* closeBrowser's own BROWSER_CLOSE_TIMEOUT_MS race settles it (ungraceful). */
|
|
36
|
+
autoAck = false;
|
|
37
|
+
send(data: string, cb?: (err?: Error) => void): void {
|
|
36
38
|
void cb;
|
|
39
|
+
if (!this.autoAck) return;
|
|
40
|
+
const { id } = JSON.parse(data) as { id: number };
|
|
41
|
+
queueMicrotask(() => this.deliver({ id, result: {} }));
|
|
37
42
|
}
|
|
38
43
|
close(): void {}
|
|
39
44
|
deliver(msg: unknown): void {
|
|
@@ -66,11 +71,12 @@ async function writePersistedPort(port: number, pid: number, savedAt: number): P
|
|
|
66
71
|
await writeFile(join(dir, 'active-port.json'), JSON.stringify({ port, pid, launched: true, savedAt }), 'utf-8');
|
|
67
72
|
}
|
|
68
73
|
|
|
69
|
-
async function connectedClient() {
|
|
74
|
+
async function connectedClient(autoAck = false) {
|
|
70
75
|
vi.resetModules();
|
|
71
76
|
const { CdpClient } = await import('../browser/cdp.js');
|
|
72
77
|
const client = new CdpClient();
|
|
73
78
|
const p = client.connect('ws://127.0.0.1:9333/devtools/page/ABC');
|
|
79
|
+
lastSocket!.autoAck = autoAck;
|
|
74
80
|
lastSocket!.emit('open');
|
|
75
81
|
await p;
|
|
76
82
|
return client;
|
|
@@ -128,6 +134,64 @@ describe('#115 review follow-up: closeBrowser() cross-process PID-kill fallback'
|
|
|
128
134
|
expect(killSpy).not.toHaveBeenCalledWith(11111, 'SIGKILL');
|
|
129
135
|
});
|
|
130
136
|
|
|
137
|
+
// The graceful path is what the monodesign detection driver actually takes,
|
|
138
|
+
// and it had no coverage: closeBrowser used to return on the Browser.close
|
|
139
|
+
// ack while Chrome was still exiting, holding its CDP port and its profile's
|
|
140
|
+
// singleton lock, with an unref'd 1s timer as the only force-kill (which
|
|
141
|
+
// never fired at all if the caller's process exited first).
|
|
142
|
+
it('graceful close waits for the process to actually exit, without killing it', async () => {
|
|
143
|
+
vi.resetModules();
|
|
144
|
+
const { closeBrowser } = await import('../browser/browser.js');
|
|
145
|
+
const port = 9337;
|
|
146
|
+
await writePersistedPort(port, 22222, Date.now());
|
|
147
|
+
|
|
148
|
+
let alive = true;
|
|
149
|
+
killSpy.mockImplementation((_pid: unknown, signal?: unknown) => {
|
|
150
|
+
if (signal === 0 && !alive) throw Object.assign(new Error('ESRCH'), { code: 'ESRCH' });
|
|
151
|
+
return true as never;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const client = await connectedClient(true);
|
|
155
|
+
const closePromise = closeBrowser(client, port);
|
|
156
|
+
// Still running a moment after the ack — close() must NOT have resolved.
|
|
157
|
+
await vi.advanceTimersByTimeAsync(200);
|
|
158
|
+
let settled = false;
|
|
159
|
+
void closePromise.then(() => {
|
|
160
|
+
settled = true;
|
|
161
|
+
});
|
|
162
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
163
|
+
expect(settled).toBe(false);
|
|
164
|
+
|
|
165
|
+
alive = false; // Chrome finishes exiting
|
|
166
|
+
await vi.advanceTimersByTimeAsync(200);
|
|
167
|
+
await closePromise;
|
|
168
|
+
expect(killSpy).not.toHaveBeenCalledWith(22222, 'SIGKILL');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('graceful close force-kills a process that never exits', async () => {
|
|
172
|
+
vi.resetModules();
|
|
173
|
+
const { closeBrowser } = await import('../browser/browser.js');
|
|
174
|
+
const port = 9338;
|
|
175
|
+
await writePersistedPort(port, 33333, Date.now());
|
|
176
|
+
|
|
177
|
+
const client = await connectedClient(true);
|
|
178
|
+
const closePromise = closeBrowser(client, port);
|
|
179
|
+
// The exit poll schedules one timer per tick, each from the previous
|
|
180
|
+
// tick's callback. How far a single advance carries through a chain like
|
|
181
|
+
// that is a fake-timer implementation detail that differs by Node version
|
|
182
|
+
// (runAllTimersAsync hung this test on Node 22 while passing on 26), so
|
|
183
|
+
// step the clock until the promise settles instead of assuming.
|
|
184
|
+
let settled = false;
|
|
185
|
+
void closePromise.then(() => {
|
|
186
|
+
settled = true;
|
|
187
|
+
});
|
|
188
|
+
for (let i = 0; i < 200 && !settled; i++) await vi.advanceTimersByTimeAsync(100);
|
|
189
|
+
await closePromise;
|
|
190
|
+
|
|
191
|
+
expect(settled).toBe(true);
|
|
192
|
+
expect(killSpy).toHaveBeenCalledWith(33333, 'SIGKILL');
|
|
193
|
+
});
|
|
194
|
+
|
|
131
195
|
it('no persisted port file at all — closeBrowser is a no-op, no kill attempted', async () => {
|
|
132
196
|
vi.resetModules();
|
|
133
197
|
const { closeBrowser } = await import('../browser/browser.js');
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/browser/actions.ts
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/browser/batch.ts
CHANGED
|
File without changes
|
package/src/browser/browser.ts
CHANGED
|
@@ -14,6 +14,11 @@ const DEFAULT_PORT = 9222;
|
|
|
14
14
|
const LAUNCH_TIMEOUT = 10_000;
|
|
15
15
|
const POLL_INTERVAL = 200;
|
|
16
16
|
const BROWSER_CLOSE_TIMEOUT_MS = 3000;
|
|
17
|
+
// How long closeBrowser() waits for the process to actually disappear before
|
|
18
|
+
// forcing it, and again after forcing. Bounded so a wedged Chrome cannot hang
|
|
19
|
+
// a caller indefinitely.
|
|
20
|
+
const PROCESS_EXIT_TIMEOUT_MS = 5000;
|
|
21
|
+
const PROCESS_EXIT_POLL_MS = 50;
|
|
17
22
|
// A cross-process persisted PID older than this is not trusted for the
|
|
18
23
|
// SIGKILL fallback in closeBrowser() — see the call site for why.
|
|
19
24
|
const PERSISTED_PID_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
@@ -79,7 +84,10 @@ async function isChromeIdentity(port: number): Promise<boolean> {
|
|
|
79
84
|
const res = await fetch(`http://127.0.0.1:${port}/json/version`, { signal: AbortSignal.timeout(1000) });
|
|
80
85
|
if (!res.ok) return false;
|
|
81
86
|
const info = (await res.json()) as { Browser?: string };
|
|
82
|
-
|
|
87
|
+
// Edge's Browser field is "Edg/<version>" with no "chrome"/"chromium"
|
|
88
|
+
// substring, even though it's Chromium-based and CDP-compatible —
|
|
89
|
+
// accept it alongside Chrome/Chromium rather than rejecting a valid target.
|
|
90
|
+
return typeof info.Browser === 'string' && /chrom(e|ium)|edg/i.test(info.Browser);
|
|
83
91
|
} catch {
|
|
84
92
|
return false;
|
|
85
93
|
}
|
|
@@ -282,6 +290,15 @@ export async function connectToTarget(port: number, targetId?: string): Promise<
|
|
|
282
290
|
* falls back to killing the tracked PID directly so a headed/interactive
|
|
283
291
|
* browser window (e.g. one spawned for a login/CAPTCHA flow) never lingers
|
|
284
292
|
* as a visible, authenticated, still-debuggable orphan process.
|
|
293
|
+
*
|
|
294
|
+
* On the graceful path this resolves only once the process is actually gone,
|
|
295
|
+
* or after force-killing it when it outlasts PROCESS_EXIT_TIMEOUT_MS. Chrome
|
|
296
|
+
* acknowledges `Browser.close` well before it exits, and this used to return
|
|
297
|
+
* on the ack with an unref'd 1s timer as the only force-kill — so it resolved
|
|
298
|
+
* while Chrome was still running and holding its port and its profile's
|
|
299
|
+
* singleton lock, and if the caller's process exited first the kill never
|
|
300
|
+
* happened at all. Callers reasonably read a resolved close() as "that
|
|
301
|
+
* browser is gone"; on the graceful path it now means that.
|
|
285
302
|
*/
|
|
286
303
|
export async function closeBrowser(client: CdpClient, port: number): Promise<void> {
|
|
287
304
|
let gracefullyClosed = false;
|
|
@@ -338,17 +355,33 @@ export async function closeBrowser(client: CdpClient, port: number): Promise<voi
|
|
|
338
355
|
return;
|
|
339
356
|
}
|
|
340
357
|
|
|
341
|
-
// Browser.close was acknowledged —
|
|
342
|
-
//
|
|
343
|
-
|
|
358
|
+
// Browser.close was acknowledged — wait for the process to actually go away,
|
|
359
|
+
// then force-kill as a safety net in case it hung.
|
|
360
|
+
if (await waitForProcessExit(pid)) return;
|
|
361
|
+
try {
|
|
362
|
+
process.kill(pid, 'SIGKILL');
|
|
363
|
+
} catch {
|
|
364
|
+
/* raced us to exit — expected */
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Poll until `pid` is gone or PROCESS_EXIT_TIMEOUT_MS elapses. Returns
|
|
369
|
+
* whether it exited. `process.kill(pid, 0)` throws once the process is no
|
|
370
|
+
* longer there, on Windows as well as POSIX. */
|
|
371
|
+
async function waitForProcessExit(pid: number): Promise<boolean> {
|
|
372
|
+
const deadline = Date.now() + PROCESS_EXIT_TIMEOUT_MS;
|
|
373
|
+
for (;;) {
|
|
344
374
|
try {
|
|
345
|
-
process.kill(pid, 0);
|
|
346
|
-
process.kill(pid, 'SIGKILL');
|
|
375
|
+
process.kill(pid, 0);
|
|
347
376
|
} catch {
|
|
348
|
-
|
|
377
|
+
return true;
|
|
349
378
|
}
|
|
350
|
-
|
|
351
|
-
|
|
379
|
+
if (Date.now() >= deadline) return false;
|
|
380
|
+
await new Promise((resolve) => {
|
|
381
|
+
const t = setTimeout(resolve, PROCESS_EXIT_POLL_MS);
|
|
382
|
+
t.unref?.();
|
|
383
|
+
});
|
|
384
|
+
}
|
|
352
385
|
}
|
|
353
386
|
|
|
354
387
|
export async function openUrl(client: CdpClient, sessionId: string, url: string): Promise<void> {
|
package/src/browser/cdp.ts
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/browser/dialog.ts
CHANGED
|
File without changes
|
package/src/browser/emulation.ts
CHANGED
|
File without changes
|
package/src/browser/find.ts
CHANGED
|
File without changes
|
package/src/browser/har.ts
CHANGED
|
File without changes
|
package/src/browser/index.ts
CHANGED
|
File without changes
|
package/src/browser/network.ts
CHANGED
|
File without changes
|
package/src/browser/pdf.ts
CHANGED
|
File without changes
|
package/src/browser/profiler.ts
CHANGED
|
File without changes
|
package/src/browser/record.ts
CHANGED
|
File without changes
|
package/src/browser/ref-cache.ts
CHANGED
|
File without changes
|
|
File without changes
|
package/src/browser/session.ts
CHANGED
|
File without changes
|
package/src/browser/snapshot.ts
CHANGED
|
File without changes
|
package/src/browser/storage.ts
CHANGED
|
File without changes
|
package/src/browser/tabs.ts
CHANGED
|
File without changes
|
package/src/browser/trace.ts
CHANGED
|
File without changes
|
package/src/browser/vitals.ts
CHANGED
|
File without changes
|
package/src/browser/wait.ts
CHANGED
|
File without changes
|
package/src/cli/action.ts
CHANGED
|
File without changes
|
package/src/cli/commands.ts
CHANGED
|
File without changes
|
package/src/cli/output.ts
CHANGED
|
File without changes
|
package/src/cli/platform.ts
CHANGED
|
File without changes
|
package/src/cli/types.ts
CHANGED
|
File without changes
|
package/src/cli.ts
CHANGED
|
File without changes
|
package/src/index.ts
CHANGED
|
File without changes
|