@monoes/monobrowse 2.24.0 → 2.24.1
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/dist/src/__tests__/browse-concurrency.test.js +33 -3
- package/dist/src/__tests__/browse-concurrency.test.js.map +1 -1
- package/dist/src/__tests__/cdp-port.test.d.ts +2 -0
- package/dist/src/__tests__/cdp-port.test.d.ts.map +1 -0
- package/dist/src/__tests__/cdp-port.test.js +61 -0
- package/dist/src/__tests__/cdp-port.test.js.map +1 -0
- package/dist/src/__tests__/profile-dir.test.js +10 -1
- package/dist/src/__tests__/profile-dir.test.js.map +1 -1
- package/dist/src/browser/bridge.d.ts +16 -4
- package/dist/src/browser/bridge.d.ts.map +1 -1
- package/dist/src/browser/bridge.js +65 -12
- package/dist/src/browser/bridge.js.map +1 -1
- package/dist/src/browser/browser-launch.d.ts.map +1 -1
- package/dist/src/browser/browser-launch.js +4 -3
- package/dist/src/browser/browser-launch.js.map +1 -1
- package/dist/src/browser/cdp-port.d.ts +19 -0
- package/dist/src/browser/cdp-port.d.ts.map +1 -0
- package/dist/src/browser/cdp-port.js +26 -0
- package/dist/src/browser/cdp-port.js.map +1 -0
- package/dist/src/browser/cdp.d.ts.map +1 -1
- package/dist/src/browser/cdp.js +2 -1
- package/dist/src/browser/cdp.js.map +1 -1
- package/dist/src/browser/index.d.ts +1 -0
- package/dist/src/browser/index.d.ts.map +1 -1
- package/dist/src/browser/index.js +1 -0
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/browser/profile-dir.d.ts +8 -0
- package/dist/src/browser/profile-dir.d.ts.map +1 -1
- package/dist/src/browser/profile-dir.js +19 -0
- package/dist/src/browser/profile-dir.js.map +1 -1
- package/dist/src/cli/action.d.ts.map +1 -1
- package/dist/src/cli/action.js +3 -2
- package/dist/src/cli/action.js.map +1 -1
- package/dist/src/cli/commands-session.d.ts.map +1 -1
- package/dist/src/cli/commands-session.js +12 -5
- package/dist/src/cli/commands-session.js.map +1 -1
- package/dist/src/cli/platform.d.ts.map +1 -1
- package/dist/src/cli/platform.js +2 -1
- package/dist/src/cli/platform.js.map +1 -1
- package/dist/src/cli.js +0 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +13 -8
- package/src/__tests__/browse-concurrency.test.ts +36 -3
- package/src/__tests__/cdp-port.test.ts +76 -0
- package/src/__tests__/profile-dir.test.ts +19 -0
- package/src/browser/bridge.ts +68 -17
- package/src/browser/browser-launch.ts +4 -2
- package/src/browser/cdp-port.ts +26 -0
- package/src/browser/cdp.ts +5 -1
- package/src/browser/index.ts +1 -0
- package/src/browser/profile-dir.ts +19 -0
- package/src/cli/action.ts +3 -2
- package/src/cli/commands-session.ts +12 -5
- package/src/cli/platform.ts +2 -1
- package/LICENSE +0 -191
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoes/monobrowse",
|
|
3
|
-
"version": "2.24.
|
|
3
|
+
"version": "2.24.1",
|
|
4
4
|
"description": "Native browser automation via Chrome DevTools Protocol — the engine powering monomind browse",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
"types": "./dist/src/index.d.ts",
|
|
14
14
|
"import": "./dist/src/index.js"
|
|
15
15
|
},
|
|
16
|
+
"./cdp-port": {
|
|
17
|
+
"types": "./dist/src/browser/cdp-port.d.ts",
|
|
18
|
+
"import": "./dist/src/browser/cdp-port.js"
|
|
19
|
+
},
|
|
16
20
|
"./cli": {
|
|
17
21
|
"types": "./dist/src/cli.d.ts",
|
|
18
22
|
"import": "./dist/src/cli.js"
|
|
@@ -33,6 +37,13 @@
|
|
|
33
37
|
"engines": {
|
|
34
38
|
"node": ">=22.12.0"
|
|
35
39
|
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc && node ../../../scripts/build-fs.mjs copy-into dist/src/browser/dashboard src/browser/dashboard/ui.html",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"test:coverage": "vitest run --coverage",
|
|
44
|
+
"prebuild": "node ../../../scripts/build-fs.mjs clean dist tsconfig.tsbuildinfo",
|
|
45
|
+
"prepublishOnly": "npm run build"
|
|
46
|
+
},
|
|
36
47
|
"keywords": [
|
|
37
48
|
"browser-automation",
|
|
38
49
|
"cdp",
|
|
@@ -59,11 +70,5 @@
|
|
|
59
70
|
"devDependencies": {
|
|
60
71
|
"typescript": "^7.0.2",
|
|
61
72
|
"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"
|
|
68
73
|
}
|
|
69
|
-
}
|
|
74
|
+
}
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { execFile, execFileSync, spawn } from 'node:child_process';
|
|
16
|
-
import { existsSync } from 'node:fs';
|
|
16
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
17
17
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
18
|
-
import { tmpdir } from 'node:os';
|
|
18
|
+
import { freemem, tmpdir } from 'node:os';
|
|
19
19
|
import { dirname, join } from 'node:path';
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
21
|
import { afterEach, beforeAll, describe, expect, it } from 'vitest';
|
|
@@ -38,7 +38,40 @@ function chromeAvailable(): boolean {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
/**
|
|
42
|
+
* #651: under host memory pressure the kernel/Chrome aborts a launching Chrome
|
|
43
|
+
* (SIGABRT), which looks like a code failure. Real Chromes alive at once: the
|
|
44
|
+
* named-session test holds three (ref, build, and the bare command's own); the
|
|
45
|
+
* rest hold two. Each headless Chrome (browser + GPU/network/renderer
|
|
46
|
+
* processes) needs on the order of 400 MiB, rounded up to 512 MiB with
|
|
47
|
+
* headroom for the warm-up Chrome and the test runner itself.
|
|
48
|
+
*/
|
|
49
|
+
const MAX_CONCURRENT_CHROMES = 3;
|
|
50
|
+
const BYTES_PER_CHROME = 512 * 1024 * 1024;
|
|
51
|
+
const REQUIRED_AVAILABLE_BYTES = MAX_CONCURRENT_CHROMES * BYTES_PER_CHROME;
|
|
52
|
+
|
|
53
|
+
/** Bytes the kernel reports as available without swapping (MemAvailable). */
|
|
54
|
+
function availableMemoryBytes(): number {
|
|
55
|
+
try {
|
|
56
|
+
const m = /^MemAvailable:\s+(\d+) kB/m.exec(readFileSync('/proc/meminfo', 'utf8'));
|
|
57
|
+
if (m) return Number(m[1]) * 1024;
|
|
58
|
+
} catch {
|
|
59
|
+
// not Linux: fall through to os.freemem()
|
|
60
|
+
}
|
|
61
|
+
return freemem();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const availableBytes = availableMemoryBytes();
|
|
65
|
+
const enoughMemory = availableBytes >= REQUIRED_AVAILABLE_BYTES;
|
|
66
|
+
const runnable = existsSync(CLI) && chromeAvailable() && enoughMemory;
|
|
67
|
+
|
|
68
|
+
if (existsSync(CLI) && !enoughMemory) {
|
|
69
|
+
const mib = (n: number) => Math.round(n / 1024 / 1024);
|
|
70
|
+
console.warn(
|
|
71
|
+
`SKIP browse-concurrency: ${mib(availableBytes)} MiB available, ` +
|
|
72
|
+
`${mib(REQUIRED_AVAILABLE_BYTES)} MiB needed for ${MAX_CONCURRENT_CHROMES} concurrent Chromes (#651)`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
42
75
|
|
|
43
76
|
interface Run {
|
|
44
77
|
code: number | null;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #666: monomind's own Chrome must not default to 9222, which mono-agent's
|
|
3
|
+
* extension bridge owns. Covers the shared port constant and its env aliases,
|
|
4
|
+
* and the bridge's fall-through from a dead relay endpoint to a live one.
|
|
5
|
+
*/
|
|
6
|
+
import type { AddressInfo } from 'node:net';
|
|
7
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
8
|
+
import { WebSocketServer } from 'ws';
|
|
9
|
+
import { BridgeTransport } from '../browser/bridge.js';
|
|
10
|
+
import { CDP_PROBE_PORTS, DEFAULT_CDP_PORT, resolveCdpPort } from '../browser/cdp-port.js';
|
|
11
|
+
|
|
12
|
+
describe('resolveCdpPort', () => {
|
|
13
|
+
it('defaults to 9422, not the bridge port', () => {
|
|
14
|
+
expect(DEFAULT_CDP_PORT).toBe(9422);
|
|
15
|
+
expect(resolveCdpPort({})).toBe(9422);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('prefers MONOBROWSE_CDP_PORT over the deprecated aliases', () => {
|
|
19
|
+
expect(
|
|
20
|
+
resolveCdpPort({
|
|
21
|
+
MONOBROWSE_CDP_PORT: '9500',
|
|
22
|
+
MONOBROWSE_PORT: '9501',
|
|
23
|
+
MONOMIND_CDP_PORT: '9502',
|
|
24
|
+
}),
|
|
25
|
+
).toBe(9500);
|
|
26
|
+
expect(resolveCdpPort({ MONOBROWSE_PORT: '9501', MONOMIND_CDP_PORT: '9502' })).toBe(9501);
|
|
27
|
+
expect(resolveCdpPort({ MONOMIND_CDP_PORT: '9502' })).toBe(9502);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('ignores a non-numeric value', () => {
|
|
31
|
+
expect(resolveCdpPort({ MONOBROWSE_CDP_PORT: 'nope' })).toBe(9422);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('probes 9422 first and keeps 9222 so a user-launched Chrome is still found', () => {
|
|
35
|
+
expect(CDP_PROBE_PORTS).toEqual([9422, 9222, 9229]);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('BridgeTransport endpoint fall-through', () => {
|
|
40
|
+
let server: WebSocketServer | undefined;
|
|
41
|
+
const saved = process.env.MONOAGENT_BRIDGE_URL;
|
|
42
|
+
|
|
43
|
+
afterEach(async () => {
|
|
44
|
+
if (saved === undefined) delete process.env.MONOAGENT_BRIDGE_URL;
|
|
45
|
+
else process.env.MONOAGENT_BRIDGE_URL = saved;
|
|
46
|
+
if (server) await new Promise((r) => server?.close(r));
|
|
47
|
+
server = undefined;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('moves past a refused endpoint to the next one', async () => {
|
|
51
|
+
server = new WebSocketServer({ port: 0, host: '127.0.0.1' });
|
|
52
|
+
await new Promise((r) => server?.once('listening', r));
|
|
53
|
+
const live = (server.address() as AddressInfo).port;
|
|
54
|
+
server.on('connection', (ws) => {
|
|
55
|
+
ws.on('message', (raw) => {
|
|
56
|
+
const env = JSON.parse(String(raw));
|
|
57
|
+
ws.send(JSON.stringify({ id: env.id, success: true, data: { tabId: 7 } }));
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
// Port 1 refuses connections; the live relay is second.
|
|
61
|
+
process.env.MONOAGENT_BRIDGE_URL = `ws://127.0.0.1:1/monoagent/cdp, ws://127.0.0.1:${live}/monoagent/cdp`;
|
|
62
|
+
|
|
63
|
+
const transport = new BridgeTransport({});
|
|
64
|
+
await transport.open({ message: () => {}, close: () => {}, error: () => {} });
|
|
65
|
+
expect(transport.attachedTabId).toBe(7);
|
|
66
|
+
transport.close();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('rejects when every endpoint is down', async () => {
|
|
70
|
+
process.env.MONOAGENT_BRIDGE_URL = 'ws://127.0.0.1:1/monoagent/cdp';
|
|
71
|
+
const transport = new BridgeTransport({});
|
|
72
|
+
await expect(
|
|
73
|
+
transport.open({ message: () => {}, close: () => {}, error: () => {} }),
|
|
74
|
+
).rejects.toThrow();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -14,6 +14,7 @@ import { tmpdir } from 'node:os';
|
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
16
16
|
import {
|
|
17
|
+
chromeSpawnEnv,
|
|
17
18
|
isOwnedProfileDir,
|
|
18
19
|
isProfileDirInUse,
|
|
19
20
|
profileDirOwnerPid,
|
|
@@ -194,3 +195,21 @@ describe('#395 isOwnedProfileDir / removeOwnedProfileDir', () => {
|
|
|
194
195
|
15_000,
|
|
195
196
|
);
|
|
196
197
|
});
|
|
198
|
+
|
|
199
|
+
describe('chromeSpawnEnv (#663)', () => {
|
|
200
|
+
const env = { TMPDIR: '/x', KEEP: '1' };
|
|
201
|
+
|
|
202
|
+
it.skipIf(process.platform === 'win32')(
|
|
203
|
+
'leaves the env alone when the singleton socket path fits',
|
|
204
|
+
() => {
|
|
205
|
+
expect(chromeSpawnEnv(env, '/tmp/mb-abcdef')).toBe(env);
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
it.skipIf(process.platform === 'win32')(
|
|
210
|
+
'redirects TMPDIR to /tmp when it would overflow the socket path',
|
|
211
|
+
() => {
|
|
212
|
+
expect(chromeSpawnEnv(env, `/tmp/${'x'.repeat(80)}`)).toEqual({ TMPDIR: '/tmp', KEEP: '1' });
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
});
|
package/src/browser/bridge.ts
CHANGED
|
@@ -38,8 +38,18 @@ import { WebSocketTransport } from './transport.js';
|
|
|
38
38
|
/** Header the Go relay authenticates with; mirrors internal/extension/token.go. */
|
|
39
39
|
export const BRIDGE_TOKEN_HEADER = 'X-Monoagent-Extension-Token';
|
|
40
40
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Relay endpoints tried in order: the extension server's default port, then
|
|
43
|
+
* the one mono-agent falls back to when 9222 is held. MONOAGENT_BRIDGE_URL
|
|
44
|
+
* (or the `url` option; comma-separated for several) overrides the list.
|
|
45
|
+
*/
|
|
46
|
+
export const DEFAULT_BRIDGE_URLS: readonly string[] = [
|
|
47
|
+
'ws://127.0.0.1:9222/monoagent/cdp',
|
|
48
|
+
'ws://127.0.0.1:9323/monoagent/cdp',
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/** The first default relay endpoint. */
|
|
52
|
+
export const DEFAULT_BRIDGE_URL = DEFAULT_BRIDGE_URLS[0];
|
|
43
53
|
|
|
44
54
|
/** How long the attach handshake may take before open() gives up. */
|
|
45
55
|
const DEFAULT_ATTACH_TIMEOUT_MS = 15_000;
|
|
@@ -67,7 +77,7 @@ export interface BridgeTransportOptions {
|
|
|
67
77
|
* to hand, only a browser with the user's session in it.
|
|
68
78
|
*/
|
|
69
79
|
tabId?: number;
|
|
70
|
-
/** Relay endpoint. Defaults to
|
|
80
|
+
/** Relay endpoint. Defaults to MONOAGENT_BRIDGE_URL, else DEFAULT_BRIDGE_URLS in order. */
|
|
71
81
|
url?: string;
|
|
72
82
|
/** Relay token, from ~/.monoagent (the Go side writes it). */
|
|
73
83
|
token?: string;
|
|
@@ -114,7 +124,10 @@ interface OutboundCdp {
|
|
|
114
124
|
export class BridgeTransport implements CdpTransport {
|
|
115
125
|
/** 0 until the extension resolves the active tab (see open()). */
|
|
116
126
|
private tabId: number;
|
|
117
|
-
private
|
|
127
|
+
private socket: CdpTransport | undefined;
|
|
128
|
+
/** Endpoints open() tries in order; empty when a socket was injected. */
|
|
129
|
+
private readonly urls: readonly string[];
|
|
130
|
+
private readonly headers: Record<string, string> | undefined;
|
|
118
131
|
private readonly attachTimeoutMs: number;
|
|
119
132
|
private handlers: CdpTransportHandlers | null = null;
|
|
120
133
|
private nextEnvelopeId = 1;
|
|
@@ -124,24 +137,62 @@ export class BridgeTransport implements CdpTransport {
|
|
|
124
137
|
constructor(opts: BridgeTransportOptions) {
|
|
125
138
|
this.tabId = opts.tabId ?? 0;
|
|
126
139
|
this.attachTimeoutMs = opts.attachTimeoutMs ?? DEFAULT_ATTACH_TIMEOUT_MS;
|
|
127
|
-
this.socket =
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
140
|
+
this.socket = opts.socket;
|
|
141
|
+
const pinned = (opts.url ?? process.env.MONOAGENT_BRIDGE_URL)
|
|
142
|
+
?.split(',')
|
|
143
|
+
.map((u) => u.trim())
|
|
144
|
+
.filter(Boolean);
|
|
145
|
+
this.urls = opts.socket ? [] : pinned?.length ? pinned : DEFAULT_BRIDGE_URLS;
|
|
146
|
+
this.headers = opts.token ? { [BRIDGE_TOKEN_HEADER]: opts.token } : undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private get sock(): CdpTransport {
|
|
150
|
+
if (!this.socket) throw new Error('extension bridge is not open');
|
|
151
|
+
return this.socket;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Open the relay socket, falling through the candidate endpoints. */
|
|
155
|
+
private async openSocket(handlers: CdpTransportHandlers): Promise<CdpTransport> {
|
|
156
|
+
const live: CdpTransportHandlers = {
|
|
157
|
+
message: handlers.message,
|
|
158
|
+
close: (err) => this.onGone(err ?? new Error('extension bridge closed')),
|
|
159
|
+
error: (err) => this.onGone(err),
|
|
160
|
+
};
|
|
161
|
+
if (this.socket) {
|
|
162
|
+
await this.socket.open(live);
|
|
163
|
+
return this.socket;
|
|
164
|
+
}
|
|
165
|
+
let last: unknown;
|
|
166
|
+
for (const url of this.urls) {
|
|
167
|
+
// A refused endpoint rejects open(); its error callback must not
|
|
168
|
+
// report the bridge gone while the next endpoint is still to try.
|
|
169
|
+
let connecting = true;
|
|
170
|
+
const socket = new WebSocketTransport(url, this.headers);
|
|
171
|
+
try {
|
|
172
|
+
await socket.open({
|
|
173
|
+
message: live.message,
|
|
174
|
+
close: (err) => !connecting && live.close(err),
|
|
175
|
+
error: (err) => !connecting && live.error(err),
|
|
176
|
+
});
|
|
177
|
+
connecting = false;
|
|
178
|
+
return socket;
|
|
179
|
+
} catch (err) {
|
|
180
|
+
last = err;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
throw last instanceof Error ? last : new Error('extension bridge unreachable');
|
|
133
184
|
}
|
|
134
185
|
|
|
135
186
|
async open(handlers: CdpTransportHandlers): Promise<void> {
|
|
136
187
|
this.handlers = handlers;
|
|
137
|
-
await this.
|
|
188
|
+
this.socket = await this.openSocket({
|
|
138
189
|
message: (data) => this.onBridgeMessage(data),
|
|
139
|
-
close: (
|
|
140
|
-
error: (
|
|
190
|
+
close: () => {},
|
|
191
|
+
error: () => {},
|
|
141
192
|
});
|
|
142
193
|
const reply = await this.command('cdp_attach', undefined, this.attachTimeoutMs);
|
|
143
194
|
if (reply.success === false) {
|
|
144
|
-
this.
|
|
195
|
+
this.sock.close();
|
|
145
196
|
const where = this.tabId ? `tab ${this.tabId}` : 'the active tab';
|
|
146
197
|
throw new Error(`bridge attach to ${where} failed: ${reply.error ?? 'unknown'}`);
|
|
147
198
|
}
|
|
@@ -149,7 +200,7 @@ export class BridgeTransport implements CdpTransport {
|
|
|
149
200
|
// the only way to learn the id when we asked for "whatever is active".
|
|
150
201
|
if (typeof reply.data?.tabId === 'number') this.tabId = reply.data.tabId;
|
|
151
202
|
if (!this.tabId) {
|
|
152
|
-
this.
|
|
203
|
+
this.sock.close();
|
|
153
204
|
throw new Error('bridge attach did not report a tab id');
|
|
154
205
|
}
|
|
155
206
|
}
|
|
@@ -199,7 +250,7 @@ export class BridgeTransport implements CdpTransport {
|
|
|
199
250
|
// attached leaves Chrome's debugging banner on the user's tab. The
|
|
200
251
|
// extension's idle sweep would eventually detach anyway.
|
|
201
252
|
void this.command('cdp_detach').catch(() => {});
|
|
202
|
-
this.socket
|
|
253
|
+
this.socket?.close();
|
|
203
254
|
this.failPending(new Error('extension bridge closed'));
|
|
204
255
|
}
|
|
205
256
|
|
|
@@ -252,7 +303,7 @@ export class BridgeTransport implements CdpTransport {
|
|
|
252
303
|
if (timer) clearTimeout(timer);
|
|
253
304
|
resolve(reply);
|
|
254
305
|
});
|
|
255
|
-
this.
|
|
306
|
+
this.sock.send(JSON.stringify(envelope)).catch((err) => {
|
|
256
307
|
if (timer) clearTimeout(timer);
|
|
257
308
|
this.pending.delete(id);
|
|
258
309
|
reject(err);
|
|
@@ -16,14 +16,15 @@ import {
|
|
|
16
16
|
} from './browser-discovery.js';
|
|
17
17
|
import { reapIdleLaunchedBrowser } from './browser-lifecycle.js';
|
|
18
18
|
import { launchedPids, launchedUserDataDirs, ownedUserDataDirPorts } from './browser-state.js';
|
|
19
|
+
import { DEFAULT_CDP_PORT } from './cdp-port.js';
|
|
19
20
|
import {
|
|
21
|
+
chromeSpawnEnv,
|
|
20
22
|
launchProfileDirPath,
|
|
21
23
|
removeOwnedProfileDir,
|
|
22
24
|
sweepStaleProfileDirs,
|
|
23
25
|
} from './profile-dir.js';
|
|
24
26
|
import type { BrowserConfig } from './types.js';
|
|
25
27
|
|
|
26
|
-
const DEFAULT_PORT = 9222;
|
|
27
28
|
const LAUNCH_TIMEOUT = 10_000;
|
|
28
29
|
const POLL_INTERVAL = 200;
|
|
29
30
|
|
|
@@ -40,7 +41,7 @@ const LAUNCH_PORT_SCAN_TRIES = 10;
|
|
|
40
41
|
class ChromeExitedEarlyError extends Error {}
|
|
41
42
|
|
|
42
43
|
export async function launchBrowser(config: BrowserConfig = {}): Promise<number> {
|
|
43
|
-
const rawPort = config.port ??
|
|
44
|
+
const rawPort = config.port ?? DEFAULT_CDP_PORT;
|
|
44
45
|
// Port 0 means "let Chrome bind a free port and tell us which" — see
|
|
45
46
|
// launchOnFreePort. Otherwise validate port is in a safe range for
|
|
46
47
|
// localhost CDP debugging.
|
|
@@ -236,6 +237,7 @@ async function launchOnFreePort(config: BrowserConfig, port: number): Promise<nu
|
|
|
236
237
|
const args = [...defaultArgs, ...callerArgs];
|
|
237
238
|
const child = spawn(chromePath, args, {
|
|
238
239
|
detached: true,
|
|
240
|
+
env: chromeSpawnEnv(process.env),
|
|
239
241
|
// stderr carries the fixed-port ownership proof (see above). The pipe is
|
|
240
242
|
// closed as soon as this launch settles, so a Chrome that outlives us
|
|
241
243
|
// never blocks writing to it.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CDP port monomind's own Chrome defaults to. Not 9222: mono-agent's
|
|
3
|
+
* extension bridge permanently owns that one and answers only /monoagent
|
|
4
|
+
* routes, so a connect to it never reaches Chrome (#666). 9422 sits outside
|
|
5
|
+
* the 922x/932x neighbourhood mono-agent uses (9222 bridge, 9232 test
|
|
6
|
+
* bridge, 9323 bridge fallback); launching still scans upward if it is busy.
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_CDP_PORT = 9422;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Ports probed by `connect --auto-connect`. 9222 stays a probe so a user's
|
|
12
|
+
* own `--remote-debugging-port=9222` Chrome is still found.
|
|
13
|
+
*/
|
|
14
|
+
export const CDP_PROBE_PORTS: readonly number[] = [DEFAULT_CDP_PORT, 9222, 9229];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The configured CDP port: MONOBROWSE_CDP_PORT, else the two older names
|
|
18
|
+
* (MONOBROWSE_PORT, MONOMIND_CDP_PORT; deprecated aliases), else the default.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveCdpPort(env: NodeJS.ProcessEnv = process.env): number {
|
|
21
|
+
for (const name of ['MONOBROWSE_CDP_PORT', 'MONOBROWSE_PORT', 'MONOMIND_CDP_PORT']) {
|
|
22
|
+
const port = Number.parseInt(env[name] ?? '', 10);
|
|
23
|
+
if (Number.isInteger(port) && port > 0) return port;
|
|
24
|
+
}
|
|
25
|
+
return DEFAULT_CDP_PORT;
|
|
26
|
+
}
|
package/src/browser/cdp.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_CDP_PORT } from './cdp-port.js';
|
|
1
2
|
import type { CdpTransport } from './transport.js';
|
|
2
3
|
import { WebSocketTransport } from './transport.js';
|
|
3
4
|
import type { CdpCommand, CdpResponse, CdpTarget } from './types.js';
|
|
@@ -233,7 +234,10 @@ export interface BrowserPage {
|
|
|
233
234
|
close(): Promise<void>;
|
|
234
235
|
}
|
|
235
236
|
|
|
236
|
-
export async function createBrowserPage(
|
|
237
|
+
export async function createBrowserPage(
|
|
238
|
+
url: string,
|
|
239
|
+
port = DEFAULT_CDP_PORT,
|
|
240
|
+
): Promise<BrowserPage> {
|
|
237
241
|
const target = await fetchNewTarget(port, url);
|
|
238
242
|
const wsDebuggerUrl = target.webSocketDebuggerUrl;
|
|
239
243
|
if (!wsDebuggerUrl) {
|
package/src/browser/index.ts
CHANGED
|
@@ -38,6 +38,25 @@ const HOLDER_WAIT_MS = 2000;
|
|
|
38
38
|
const POLL_MS = 50;
|
|
39
39
|
const RM_RETRY_DELAY_MS = 250;
|
|
40
40
|
|
|
41
|
+
/** Longest unix socket path Chrome's singleton lock can bind (sun_path is 104
|
|
42
|
+
* bytes on macOS, 108 on Linux; both include the NUL). */
|
|
43
|
+
const MAX_SOCKET_PATH = 103;
|
|
44
|
+
/** `/org.chromium.Chromium.XXXXXX/SingletonSocket` — what Chrome appends to TMPDIR. */
|
|
45
|
+
const SINGLETON_SOCKET_SUFFIX_LEN = '/org.chromium.Chromium.XXXXXX/SingletonSocket'.length;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Environment to spawn Chrome with (#663). Chrome binds its singleton lock
|
|
49
|
+
* socket under $TMPDIR regardless of --user-data-dir, and aborts at startup
|
|
50
|
+
* (`FATAL: Socket path too long`, SIGABRT) once that path passes the unix
|
|
51
|
+
* socket limit — which a deep TMPDIR (CI work dirs, sandboxed runners) does.
|
|
52
|
+
* Only then is TMPDIR redirected to /tmp; otherwise the env is untouched.
|
|
53
|
+
*/
|
|
54
|
+
export function chromeSpawnEnv(env: NodeJS.ProcessEnv, tmp = tmpdir()): NodeJS.ProcessEnv {
|
|
55
|
+
if (process.platform === 'win32') return env;
|
|
56
|
+
if (Buffer.byteLength(tmp) + SINGLETON_SOCKET_SUFFIX_LEN <= MAX_SOCKET_PATH) return env;
|
|
57
|
+
return { ...env, TMPDIR: '/tmp' };
|
|
58
|
+
}
|
|
59
|
+
|
|
41
60
|
/** Name for a new default launch profile, directly under `root`. */
|
|
42
61
|
export function launchProfileDirPath(port: number, id: string, root = tmpdir()): string {
|
|
43
62
|
return join(root, `monomind-browser-${port}-${process.pid}-${id}`);
|
package/src/cli/action.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { existsSync } from 'node:fs';
|
|
|
4
4
|
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { Command } from 'commander';
|
|
7
|
+
import { resolveCdpPort } from '../browser/cdp-port.js';
|
|
7
8
|
import type { ActionDef, StepDef } from '../index.js';
|
|
8
9
|
import { type AnalyzerPage, analyzePageForAction } from '../index.js';
|
|
9
10
|
|
|
@@ -72,7 +73,7 @@ export function createActionCommand(): Command {
|
|
|
72
73
|
| null;
|
|
73
74
|
if (!createBrowserPage) {
|
|
74
75
|
throw new Error(
|
|
75
|
-
'Browser CDP client not available. Ensure Chrome is running with --remote-debugging-port=
|
|
76
|
+
'Browser CDP client not available. Ensure Chrome is running with --remote-debugging-port=9422',
|
|
76
77
|
);
|
|
77
78
|
}
|
|
78
79
|
const page = await createBrowserPage(opts.url);
|
|
@@ -119,7 +120,7 @@ export function createActionCommand(): Command {
|
|
|
119
120
|
const { clickElement, fillElement, evaluateJs } = await import('../browser/actions.js');
|
|
120
121
|
const { findBySelector } = await import('../browser/find.js');
|
|
121
122
|
const { waitFor } = await import('../browser/wait.js');
|
|
122
|
-
const port =
|
|
123
|
+
const port = resolveCdpPort();
|
|
123
124
|
const { client, sessionId } = await connectToTarget(port);
|
|
124
125
|
const refs = new Map<string, import('../browser/types.js').ElementRef>();
|
|
125
126
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* differently: it must never kill a Chrome it did not launch.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { CDP_PROBE_PORTS, DEFAULT_CDP_PORT } from '../browser/cdp-port.js';
|
|
11
12
|
import { detectAttentionNeeded, switchToHeaded } from './attention-wall.js';
|
|
12
13
|
import { output } from './output.js';
|
|
13
14
|
import {
|
|
@@ -299,22 +300,28 @@ export const closeCommand: Command = {
|
|
|
299
300
|
export const connectCommand: Command = {
|
|
300
301
|
name: 'connect',
|
|
301
302
|
description:
|
|
302
|
-
'Connect to existing Chrome instance; later commands reuse this session (note: `open` without --port still launches on its own default). Usage: monomind browse connect [--port
|
|
303
|
+
'Connect to existing Chrome instance; later commands reuse this session (note: `open` without --port still launches on its own default). Usage: monomind browse connect [--port 9422] [--target <id>] [--auto-connect]',
|
|
303
304
|
options: [
|
|
304
|
-
{
|
|
305
|
+
{
|
|
306
|
+
name: 'port',
|
|
307
|
+
short: 'p',
|
|
308
|
+
type: 'number',
|
|
309
|
+
description: 'CDP port',
|
|
310
|
+
default: DEFAULT_CDP_PORT,
|
|
311
|
+
},
|
|
305
312
|
{ name: 'target', type: 'string', description: 'Target ID to attach to' },
|
|
306
313
|
{
|
|
307
314
|
name: 'auto-connect',
|
|
308
315
|
type: 'boolean',
|
|
309
|
-
description: 'Auto-discover running Chrome on ports 9222 and 9229',
|
|
316
|
+
description: 'Auto-discover running Chrome on ports 9422, 9222 and 9229',
|
|
310
317
|
default: false,
|
|
311
318
|
},
|
|
312
319
|
],
|
|
313
320
|
action: async (ctx: CommandContext): Promise<CommandResult> => {
|
|
314
|
-
let port = (ctx.flags.port as number) ??
|
|
321
|
+
let port = (ctx.flags.port as number) ?? DEFAULT_CDP_PORT;
|
|
315
322
|
|
|
316
323
|
if (ctx.flags['auto-connect']) {
|
|
317
|
-
const probePorts =
|
|
324
|
+
const probePorts = CDP_PROBE_PORTS;
|
|
318
325
|
let found = false;
|
|
319
326
|
for (const p of probePorts) {
|
|
320
327
|
try {
|
package/src/cli/platform.ts
CHANGED
|
@@ -8,9 +8,10 @@ import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { Command } from 'commander';
|
|
11
|
+
import { resolveCdpPort } from '../browser/cdp-port.js';
|
|
11
12
|
import { getAdapter } from '../index.js';
|
|
12
13
|
|
|
13
|
-
const CDP_PORT =
|
|
14
|
+
const CDP_PORT = resolveCdpPort();
|
|
14
15
|
|
|
15
16
|
const SESSIONS_FILE = join(homedir(), '.monomind', 'sessions.json');
|
|
16
17
|
|