@monoes/monobrowse 1.0.19 → 1.0.22
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.d.ts +15 -0
- package/dist/src/__tests__/browse-concurrency.test.d.ts.map +1 -0
- package/dist/src/__tests__/browse-concurrency.test.js +198 -0
- package/dist/src/__tests__/browse-concurrency.test.js.map +1 -0
- package/dist/src/__tests__/cli-version.test.d.ts +20 -0
- package/dist/src/__tests__/cli-version.test.d.ts.map +1 -0
- package/dist/src/__tests__/cli-version.test.js +45 -0
- package/dist/src/__tests__/cli-version.test.js.map +1 -0
- package/dist/src/__tests__/close-browser.test.js +2 -2
- package/dist/src/__tests__/close-browser.test.js.map +1 -1
- package/dist/src/__tests__/frame-command.test.js +12 -2
- package/dist/src/__tests__/frame-command.test.js.map +1 -1
- package/dist/src/__tests__/reap-idle-browser.test.js +13 -13
- package/dist/src/__tests__/reap-idle-browser.test.js.map +1 -1
- package/dist/src/__tests__/ref-cache.test.js +96 -58
- package/dist/src/__tests__/ref-cache.test.js.map +1 -1
- package/dist/src/__tests__/report-exit-code.test.js +1 -1
- package/dist/src/__tests__/session-discovery.test.d.ts +13 -0
- package/dist/src/__tests__/session-discovery.test.d.ts.map +1 -0
- package/dist/src/__tests__/session-discovery.test.js +218 -0
- package/dist/src/__tests__/session-discovery.test.js.map +1 -0
- package/dist/src/browser/browser.d.ts +2 -1
- package/dist/src/browser/browser.d.ts.map +1 -1
- package/dist/src/browser/browser.js +17 -9
- package/dist/src/browser/browser.js.map +1 -1
- package/dist/src/browser/ref-cache.d.ts +50 -22
- package/dist/src/browser/ref-cache.d.ts.map +1 -1
- package/dist/src/browser/ref-cache.js +122 -57
- package/dist/src/browser/ref-cache.js.map +1 -1
- package/dist/src/cli/commands-context.js +1 -1
- package/dist/src/cli/commands-context.js.map +1 -1
- package/dist/src/cli/commands-navigate.js +2 -2
- package/dist/src/cli/commands-navigate.js.map +1 -1
- package/dist/src/cli/commands-page.js +2 -2
- package/dist/src/cli/commands-page.js.map +1 -1
- package/dist/src/cli/commands-report.js +2 -2
- package/dist/src/cli/commands-report.js.map +1 -1
- package/dist/src/cli/commands-session.d.ts.map +1 -1
- package/dist/src/cli/commands-session.js +70 -56
- package/dist/src/cli/commands-session.js.map +1 -1
- package/dist/src/cli/commands.d.ts.map +1 -1
- package/dist/src/cli/commands.js +102 -66
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/cli/session.d.ts +66 -2
- package/dist/src/cli/session.d.ts.map +1 -1
- package/dist/src/cli/session.js +190 -33
- package/dist/src/cli/session.js.map +1 -1
- package/dist/src/cli.js +3 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/__tests__/browse-concurrency.test.ts +233 -0
- package/src/__tests__/cli-version.test.ts +50 -0
- package/src/__tests__/close-browser.test.ts +2 -2
- package/src/__tests__/frame-command.test.ts +12 -2
- package/src/__tests__/reap-idle-browser.test.ts +15 -14
- package/src/__tests__/ref-cache.test.ts +112 -59
- package/src/__tests__/report-exit-code.test.ts +1 -1
- package/src/__tests__/session-discovery.test.ts +259 -0
- package/src/browser/browser.ts +23 -9
- package/src/browser/ref-cache.ts +144 -72
- package/src/cli/commands-context.ts +1 -1
- package/src/cli/commands-navigate.ts +2 -2
- package/src/cli/commands-page.ts +2 -2
- package/src/cli/commands-report.ts +2 -2
- package/src/cli/commands-session.ts +74 -55
- package/src/cli/commands.ts +106 -66
- package/src/cli/session.ts +208 -33
- package/src/cli.ts +3 -1
package/src/cli/commands.ts
CHANGED
|
@@ -90,6 +90,7 @@ import {
|
|
|
90
90
|
} from './commands-trace.js';
|
|
91
91
|
import { output } from './output.js';
|
|
92
92
|
import { createPlatformCommand } from './platform.js';
|
|
93
|
+
import { applySessionPortFlag } from './session.js';
|
|
93
94
|
import type { Command, CommandContext, CommandResult } from './types.js';
|
|
94
95
|
|
|
95
96
|
// Re-exported for direct unit testing — these were exported from this file
|
|
@@ -212,80 +213,119 @@ const batchCommand: Command = {
|
|
|
212
213
|
const actionSubcommand: Command = wrapCommanderCommand(createActionCommand);
|
|
213
214
|
const platformSubcommand: Command = wrapCommanderCommand(createPlatformCommand);
|
|
214
215
|
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// Session selector
|
|
218
|
+
//
|
|
219
|
+
// Sessions are per-port (#318), so `--port N` is how any command says which
|
|
220
|
+
// one it means; without it a command resolves the newest live session in this
|
|
221
|
+
// directory. Wiring it here, once, gives every subcommand the selector
|
|
222
|
+
// without each action having to read the flag: session.port is set before the
|
|
223
|
+
// action runs, and that is what every ensureConnected(session.port) resolves.
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
const PORT_OPTION = {
|
|
227
|
+
name: 'port',
|
|
228
|
+
short: 'p',
|
|
229
|
+
type: 'number',
|
|
230
|
+
description: 'Act on the browse session on this CDP port (default: the newest live session)',
|
|
231
|
+
} as const;
|
|
232
|
+
|
|
233
|
+
function withSessionSelector(cmd: Command): Command {
|
|
234
|
+
const action = cmd.action;
|
|
235
|
+
return {
|
|
236
|
+
...cmd,
|
|
237
|
+
options: cmd.options?.some((o) => o.name === 'port')
|
|
238
|
+
? cmd.options
|
|
239
|
+
: [...(cmd.options ?? []), { ...PORT_OPTION }],
|
|
240
|
+
action: action
|
|
241
|
+
? async (ctx: CommandContext) => {
|
|
242
|
+
applySessionPortFlag(ctx.flags);
|
|
243
|
+
return action(ctx);
|
|
244
|
+
}
|
|
245
|
+
: undefined,
|
|
246
|
+
subcommands: cmd.subcommands?.map(withSessionSelector),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
215
250
|
// ---------------------------------------------------------------------------
|
|
216
251
|
// Root browse command
|
|
217
252
|
// ---------------------------------------------------------------------------
|
|
218
253
|
|
|
254
|
+
// Every subcommand gets the `--port` session selector (see
|
|
255
|
+
// withSessionSelector). batchCommand dispatches over this same array, so a
|
|
256
|
+
// batched command honours the selector too.
|
|
257
|
+
const subcommands: Command[] = [
|
|
258
|
+
openCommand,
|
|
259
|
+
snapshotCommand,
|
|
260
|
+
clickCommand,
|
|
261
|
+
dblclickCommand,
|
|
262
|
+
fillCommand,
|
|
263
|
+
typeCommand,
|
|
264
|
+
pressCommand,
|
|
265
|
+
keyboardCommand,
|
|
266
|
+
keydownCommand,
|
|
267
|
+
keyupCommand,
|
|
268
|
+
hoverCommand,
|
|
269
|
+
focusCommand,
|
|
270
|
+
selectCommand,
|
|
271
|
+
checkCommand,
|
|
272
|
+
uncheckCommand,
|
|
273
|
+
isvisibleCommand,
|
|
274
|
+
isenabledCommand,
|
|
275
|
+
ischeckedCommand,
|
|
276
|
+
tapCommand,
|
|
277
|
+
swipeCommand,
|
|
278
|
+
scrollIntoViewCommand,
|
|
279
|
+
dragCommand,
|
|
280
|
+
uploadCommand,
|
|
281
|
+
downloadCommand,
|
|
282
|
+
mouseCommand,
|
|
283
|
+
clipboardCommand,
|
|
284
|
+
waitCommand,
|
|
285
|
+
screenshotCommand,
|
|
286
|
+
getCommand,
|
|
287
|
+
scrollCommand,
|
|
288
|
+
navigateCommand,
|
|
289
|
+
setCommand,
|
|
290
|
+
stateCommand,
|
|
291
|
+
networkCommand,
|
|
292
|
+
evalCommand,
|
|
293
|
+
dialogCommand,
|
|
294
|
+
frameCommand,
|
|
295
|
+
tabCommand,
|
|
296
|
+
windowCommand,
|
|
297
|
+
consoleLogCommand,
|
|
298
|
+
errorsCommand,
|
|
299
|
+
storageCommand,
|
|
300
|
+
cookiesCommand,
|
|
301
|
+
pdfCommand,
|
|
302
|
+
isCommand,
|
|
303
|
+
findCommand,
|
|
304
|
+
highlightCommand,
|
|
305
|
+
diffCommand,
|
|
306
|
+
pushstateCommand,
|
|
307
|
+
batchCommand,
|
|
308
|
+
addinitscriptCommand,
|
|
309
|
+
removeinitscriptCommand,
|
|
310
|
+
connectCommand,
|
|
311
|
+
recordCommand,
|
|
312
|
+
traceCommand,
|
|
313
|
+
profilerCommand,
|
|
314
|
+
vitalsCommand,
|
|
315
|
+
reportCommand,
|
|
316
|
+
harCommand,
|
|
317
|
+
resizeCommand,
|
|
318
|
+
closeCommand,
|
|
319
|
+
actionSubcommand,
|
|
320
|
+
platformSubcommand,
|
|
321
|
+
].map(withSessionSelector);
|
|
322
|
+
|
|
219
323
|
const browseCommand: Command = {
|
|
220
324
|
name: 'browse',
|
|
221
325
|
description: 'Native browser automation via Chrome DevTools Protocol',
|
|
222
|
-
subcommands
|
|
223
|
-
openCommand,
|
|
224
|
-
snapshotCommand,
|
|
225
|
-
clickCommand,
|
|
226
|
-
dblclickCommand,
|
|
227
|
-
fillCommand,
|
|
228
|
-
typeCommand,
|
|
229
|
-
pressCommand,
|
|
230
|
-
keyboardCommand,
|
|
231
|
-
keydownCommand,
|
|
232
|
-
keyupCommand,
|
|
233
|
-
hoverCommand,
|
|
234
|
-
focusCommand,
|
|
235
|
-
selectCommand,
|
|
236
|
-
checkCommand,
|
|
237
|
-
uncheckCommand,
|
|
238
|
-
isvisibleCommand,
|
|
239
|
-
isenabledCommand,
|
|
240
|
-
ischeckedCommand,
|
|
241
|
-
tapCommand,
|
|
242
|
-
swipeCommand,
|
|
243
|
-
scrollIntoViewCommand,
|
|
244
|
-
dragCommand,
|
|
245
|
-
uploadCommand,
|
|
246
|
-
downloadCommand,
|
|
247
|
-
mouseCommand,
|
|
248
|
-
clipboardCommand,
|
|
249
|
-
waitCommand,
|
|
250
|
-
screenshotCommand,
|
|
251
|
-
getCommand,
|
|
252
|
-
scrollCommand,
|
|
253
|
-
navigateCommand,
|
|
254
|
-
setCommand,
|
|
255
|
-
stateCommand,
|
|
256
|
-
networkCommand,
|
|
257
|
-
evalCommand,
|
|
258
|
-
dialogCommand,
|
|
259
|
-
frameCommand,
|
|
260
|
-
tabCommand,
|
|
261
|
-
windowCommand,
|
|
262
|
-
consoleLogCommand,
|
|
263
|
-
errorsCommand,
|
|
264
|
-
storageCommand,
|
|
265
|
-
cookiesCommand,
|
|
266
|
-
pdfCommand,
|
|
267
|
-
isCommand,
|
|
268
|
-
findCommand,
|
|
269
|
-
highlightCommand,
|
|
270
|
-
diffCommand,
|
|
271
|
-
pushstateCommand,
|
|
272
|
-
batchCommand,
|
|
273
|
-
addinitscriptCommand,
|
|
274
|
-
removeinitscriptCommand,
|
|
275
|
-
connectCommand,
|
|
276
|
-
recordCommand,
|
|
277
|
-
traceCommand,
|
|
278
|
-
profilerCommand,
|
|
279
|
-
vitalsCommand,
|
|
280
|
-
reportCommand,
|
|
281
|
-
harCommand,
|
|
282
|
-
resizeCommand,
|
|
283
|
-
closeCommand,
|
|
284
|
-
actionSubcommand,
|
|
285
|
-
platformSubcommand,
|
|
286
|
-
],
|
|
326
|
+
subcommands,
|
|
287
327
|
options: [
|
|
288
|
-
{
|
|
328
|
+
{ ...PORT_OPTION },
|
|
289
329
|
{ name: 'session', short: 's', type: 'string', description: 'Named session to use' },
|
|
290
330
|
],
|
|
291
331
|
examples: [
|
package/src/cli/session.ts
CHANGED
|
@@ -7,13 +7,36 @@
|
|
|
7
7
|
* module-level. It is a mutable record rather than separate bindings because
|
|
8
8
|
* the subcommand modules that update it import it (see `session`).
|
|
9
9
|
*
|
|
10
|
+
* THE SESSION RULE (#318). A browse session is one Chrome plus the CDP port
|
|
11
|
+
* it listens on, recorded per working directory in
|
|
12
|
+
* `.monomind/monobrowse/sessions/<port>.json`:
|
|
13
|
+
*
|
|
14
|
+
* - `open` with no `--port` STARTS its own session: Chrome binds a
|
|
15
|
+
* kernel-assigned free port in a profile directory of its own. It never
|
|
16
|
+
* joins an existing one, so two uncoordinated invocations can never land
|
|
17
|
+
* on the same port or profile. `open` reports the port it got.
|
|
18
|
+
* - `open`/`connect` WITH `--port N` keep the old behaviour exactly: attach
|
|
19
|
+
* to a Chrome already listening on N, otherwise launch there.
|
|
20
|
+
* - Any later command with no `--port` acts on the newest session in this
|
|
21
|
+
* directory whose browser still answers; every dead record it passes on
|
|
22
|
+
* the way is dropped (self-heal). With none left it starts a session of
|
|
23
|
+
* its own, exactly as `open` would.
|
|
24
|
+
* - Any later command with `--port N` acts on that session, which is how a
|
|
25
|
+
* concurrent caller pins the one `open` handed it.
|
|
26
|
+
* - `close` ends exactly the session it resolved — one browser, one record.
|
|
27
|
+
*
|
|
10
28
|
* Split out of commands.ts, which is now the command catalogue.
|
|
11
29
|
*/
|
|
12
30
|
|
|
13
|
-
import
|
|
31
|
+
import { randomUUID } from 'node:crypto';
|
|
32
|
+
import { tmpdir } from 'node:os';
|
|
33
|
+
import { join } from 'node:path';
|
|
34
|
+
import type { CdpClient, ElementRef, SessionRecord } from '../index.js';
|
|
14
35
|
import { output } from './output.js';
|
|
36
|
+
import type { ParsedFlags } from './types.js';
|
|
15
37
|
|
|
16
|
-
|
|
38
|
+
/** `session.port` when no `--port` was given: resolve or start one instead. */
|
|
39
|
+
const UNPINNED = 0;
|
|
17
40
|
|
|
18
41
|
// Runtime state (single session per CLI process).
|
|
19
42
|
//
|
|
@@ -33,11 +56,36 @@ export const session: {
|
|
|
33
56
|
client: null,
|
|
34
57
|
sessionId: '',
|
|
35
58
|
targetId: '',
|
|
36
|
-
port:
|
|
59
|
+
port: UNPINNED,
|
|
37
60
|
refs: new Map(),
|
|
38
61
|
parentSessionId: '',
|
|
39
62
|
};
|
|
40
63
|
|
|
64
|
+
/**
|
|
65
|
+
* The session the caller pinned with `--port`, or undefined for "whichever
|
|
66
|
+
* session this directory's store resolves to". Anything unusable — absent,
|
|
67
|
+
* out of range, unparseable — reads as undefined.
|
|
68
|
+
*/
|
|
69
|
+
export function pinnedPort(flags: ParsedFlags): number | undefined {
|
|
70
|
+
const raw = flags.port;
|
|
71
|
+
const port = typeof raw === 'number' ? raw : typeof raw === 'string' ? Number(raw) : Number.NaN;
|
|
72
|
+
return Number.isInteger(port) && port >= 1024 && port <= 65535 ? port : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Pin this process to the session named by `--port` before the subcommand
|
|
77
|
+
* runs (see the wrapper in commands.ts), so every command that resolves its
|
|
78
|
+
* session through `session.port` honours the selector.
|
|
79
|
+
*/
|
|
80
|
+
export function applySessionPortFlag(flags: ParsedFlags): void {
|
|
81
|
+
// Only ever pins: a command with no --port must not reset the port of a
|
|
82
|
+
// session this process already established (`batch "open …" "snapshot"`
|
|
83
|
+
// runs both actions in one process, and the second must stay on the
|
|
84
|
+
// session the first started).
|
|
85
|
+
const pinned = pinnedPort(flags);
|
|
86
|
+
if (pinned !== undefined) session.port = pinned;
|
|
87
|
+
}
|
|
88
|
+
|
|
41
89
|
export async function getBrowser() {
|
|
42
90
|
return import('../index.js');
|
|
43
91
|
}
|
|
@@ -85,35 +133,151 @@ export function ensureSignalCleanupHandlers(): void {
|
|
|
85
133
|
}
|
|
86
134
|
}
|
|
87
135
|
|
|
88
|
-
// Each CLI invocation is a fresh process, so
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
export async function
|
|
136
|
+
// Each CLI invocation is a fresh process, so the session a previous `open`
|
|
137
|
+
// started only survives as its record on disk. Walk the recorded sessions
|
|
138
|
+
// newest-first and return the first whose browser still answers — that is
|
|
139
|
+
// "the" session for a command that did not pin one with `--port`.
|
|
140
|
+
//
|
|
141
|
+
// Records whose browser is gone are dropped as we pass them (self-heal): a
|
|
142
|
+
// crashed or manually-killed Chrome must not wedge every later command.
|
|
143
|
+
export async function resolveLiveSession(
|
|
96
144
|
browser: Awaited<ReturnType<typeof getBrowser>>,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
} catch {
|
|
110
|
-
await browser.clearActivePort();
|
|
145
|
+
opts: { strict?: boolean } = {},
|
|
146
|
+
): Promise<SessionRecord | null> {
|
|
147
|
+
for (const record of await browser.listSessionRecords()) {
|
|
148
|
+
if (await cdpAnswers(record.port)) return record;
|
|
149
|
+
await browser.removeSessionRecord(record.port);
|
|
150
|
+
await browser.clearRefCache(record.port);
|
|
151
|
+
if (!record.launched && opts.strict !== false) {
|
|
152
|
+
// connect-origin port: the browser belonged to someone else. Silently
|
|
153
|
+
// launching our own headless Chrome on that port would squat the
|
|
154
|
+
// user's debug port and swap which browser commands act on — fail
|
|
155
|
+
// loudly instead. `close` passes strict:false; it has nothing to
|
|
156
|
+
// attach to and just cleans up.
|
|
111
157
|
throw new Error(
|
|
112
|
-
`Connected browser on port ${
|
|
158
|
+
`Connected browser on port ${record.port} is gone — re-run \`connect\` (or \`open\` to launch a fresh one).`,
|
|
113
159
|
);
|
|
114
160
|
}
|
|
115
161
|
}
|
|
116
|
-
|
|
162
|
+
// Nothing native is live. A session opened by a pre-#318 CLI in this
|
|
163
|
+
// directory may still be — it is the oldest possible candidate, so it is
|
|
164
|
+
// considered last.
|
|
165
|
+
return adoptLegacySession(browser);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Take over the single session a pre-#318 CLI recorded in this directory, so
|
|
170
|
+
* upgrading monobrowse mid-session does not strand its Chrome with no handle
|
|
171
|
+
* anyone can use. Returns the session — now an ordinary per-port record that
|
|
172
|
+
* `snapshot`, `--port` and `close` treat like any other — or null.
|
|
173
|
+
*
|
|
174
|
+
* `port` restricts adoption to a session the caller already named, so a
|
|
175
|
+
* legacy record for some other port is left for its own resolution.
|
|
176
|
+
*
|
|
177
|
+
* A record that a per-port record already supersedes, or whose browser no
|
|
178
|
+
* longer answers, is simply dropped: the same self-heal as for a dead native
|
|
179
|
+
* record, and nothing is ever written back to the old layout. This is the
|
|
180
|
+
* only place the legacy file is read, and `open` never consults it — a bare
|
|
181
|
+
* `open` still always starts a session of its own.
|
|
182
|
+
*/
|
|
183
|
+
export async function adoptLegacySession(
|
|
184
|
+
browser: Awaited<ReturnType<typeof getBrowser>>,
|
|
185
|
+
opts: { port?: number } = {},
|
|
186
|
+
): Promise<SessionRecord | null> {
|
|
187
|
+
const legacy = await browser.loadLegacySessionRecord();
|
|
188
|
+
if (!legacy) return null;
|
|
189
|
+
if (opts.port !== undefined && opts.port !== legacy.port) return null;
|
|
190
|
+
if (await browser.loadSessionRecord(legacy.port)) {
|
|
191
|
+
await browser.removeLegacySessionRecord();
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
if (!(await cdpAnswers(legacy.port))) {
|
|
195
|
+
await browser.removeLegacySessionRecord();
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
await browser.saveSessionRecord(legacy.port, {
|
|
199
|
+
launched: legacy.launched,
|
|
200
|
+
pid: legacy.pid,
|
|
201
|
+
userDataDir: legacy.userDataDir,
|
|
202
|
+
savedAt: legacy.savedAt,
|
|
203
|
+
});
|
|
204
|
+
await browser.removeLegacySessionRecord();
|
|
205
|
+
return legacy;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function cdpAnswers(port: number): Promise<boolean> {
|
|
209
|
+
try {
|
|
210
|
+
const res = await fetch(`http://127.0.0.1:${port}/json/version`, {
|
|
211
|
+
signal: AbortSignal.timeout(1500),
|
|
212
|
+
});
|
|
213
|
+
return res.ok;
|
|
214
|
+
} catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Start or attach to the browser this session drives, and record it.
|
|
221
|
+
*
|
|
222
|
+
* With a `port`, that is the caller's explicit `--port`: attach to a Chrome
|
|
223
|
+
* already listening there, else launch there (unchanged behaviour). Without
|
|
224
|
+
* one, Chrome binds a kernel-assigned free port in a profile directory of
|
|
225
|
+
* this session's own — the allocation that makes two concurrent `open`
|
|
226
|
+
* invocations independent (#318).
|
|
227
|
+
*/
|
|
228
|
+
export async function launchSessionBrowser(
|
|
229
|
+
browser: Awaited<ReturnType<typeof getBrowser>>,
|
|
230
|
+
opts: { port?: number; headless: boolean },
|
|
231
|
+
): Promise<number> {
|
|
232
|
+
const port = opts.port
|
|
233
|
+
? await browser.launchBrowser({ port: opts.port, headless: opts.headless })
|
|
234
|
+
: await browser.launchBrowser({
|
|
235
|
+
port: 0,
|
|
236
|
+
headless: opts.headless,
|
|
237
|
+
// Chrome reports the port it bound inside this directory, so it must
|
|
238
|
+
// belong to this session alone.
|
|
239
|
+
userDataDir: join(tmpdir(), `monomind-browse-${process.pid}-${randomUUID().slice(0, 8)}`),
|
|
240
|
+
});
|
|
241
|
+
await recordSession(browser, port);
|
|
242
|
+
ensureSignalCleanupHandlers();
|
|
243
|
+
return port;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Persist the session so later CLI invocations (each a fresh process) can
|
|
247
|
+
// find this browser, together with the launched PID/userDataDir so their
|
|
248
|
+
// closeBrowser can still kill it even though launchedPids (browser.ts) is
|
|
249
|
+
// per-process and empty there.
|
|
250
|
+
//
|
|
251
|
+
// launchBrowser() can either LAUNCH a fresh Chrome or ATTACH to one already
|
|
252
|
+
// listening on an explicitly requested port (see its own "attach if already
|
|
253
|
+
// Chrome" comment) — it returns only a port number, with no signal telling
|
|
254
|
+
// this caller which happened. getLaunchedPid(port) is undefined on the
|
|
255
|
+
// attach path (this process never spawned anything). Unconditionally saving
|
|
256
|
+
// {pid: undefined} on attach used to CLOBBER a real PID a previous `open`
|
|
257
|
+
// had persisted for this exact port, destroying the only way a later
|
|
258
|
+
// process's closeBrowser() PID-kill fallback could ever find it.
|
|
259
|
+
async function recordSession(
|
|
260
|
+
browser: Awaited<ReturnType<typeof getBrowser>>,
|
|
261
|
+
port: number,
|
|
262
|
+
): Promise<void> {
|
|
263
|
+
const pid = browser.getLaunchedPid(port);
|
|
264
|
+
if (pid !== undefined) {
|
|
265
|
+
await browser.saveSessionRecord(port, {
|
|
266
|
+
pid,
|
|
267
|
+
userDataDir: browser.getLaunchedUserDataDir(port),
|
|
268
|
+
});
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
// A pre-#318 record for this exact port counts as the existing one, so
|
|
272
|
+
// attaching to a session an older CLI opened keeps its provenance (a
|
|
273
|
+
// `connect`ed browser stays launched:false and is never killed).
|
|
274
|
+
const existing =
|
|
275
|
+
(await browser.loadSessionRecord(port)) ?? (await adoptLegacySession(browser, { port }));
|
|
276
|
+
await browser.saveSessionRecord(port, {
|
|
277
|
+
launched: existing?.launched,
|
|
278
|
+
pid: existing?.pid,
|
|
279
|
+
userDataDir: existing?.userDataDir,
|
|
280
|
+
});
|
|
117
281
|
}
|
|
118
282
|
|
|
119
283
|
export async function ensureConnected(port: number, targetId?: string) {
|
|
@@ -126,8 +290,10 @@ export async function ensureConnected(port: number, targetId?: string) {
|
|
|
126
290
|
browser.teardownConsoleCapture(session.sessionId);
|
|
127
291
|
session.client.close();
|
|
128
292
|
}
|
|
129
|
-
|
|
130
|
-
|
|
293
|
+
// Pinned port, else the newest live session, else a session of our own —
|
|
294
|
+
// a command run before any `open` still just works.
|
|
295
|
+
const pinned = port > 0 ? port : ((await resolveLiveSession(browser))?.port ?? undefined);
|
|
296
|
+
session.port = await launchSessionBrowser(browser, { port: pinned, headless: true });
|
|
131
297
|
const conn = await browser.connectToTarget(session.port, targetId);
|
|
132
298
|
session.client = conn.client;
|
|
133
299
|
session.sessionId = conn.sessionId;
|
|
@@ -159,7 +325,7 @@ export async function hydrateRefsFromCache(
|
|
|
159
325
|
targetId: string,
|
|
160
326
|
currentUrl: string,
|
|
161
327
|
): Promise<void> {
|
|
162
|
-
const cached = await browser.loadRefCache(targetId);
|
|
328
|
+
const cached = await browser.loadRefCache(session.port, targetId);
|
|
163
329
|
if (!cached) return;
|
|
164
330
|
if (currentUrl && cached.url && currentUrl !== cached.url) {
|
|
165
331
|
output.printError(
|
|
@@ -264,10 +430,19 @@ export async function switchToHeaded(url: string, port: number): Promise<void> {
|
|
|
264
430
|
session.targetId = '';
|
|
265
431
|
}
|
|
266
432
|
|
|
267
|
-
//
|
|
268
|
-
|
|
433
|
+
// Kernel-assigned free port and a profile of its own, so this temporary
|
|
434
|
+
// headed window cannot collide with any other session (#318) — a
|
|
435
|
+
// neighbouring port (port + 10) could well be another session's.
|
|
436
|
+
let headedPort = 0;
|
|
269
437
|
try {
|
|
270
|
-
await browser.launchBrowser({
|
|
438
|
+
headedPort = await browser.launchBrowser({
|
|
439
|
+
port: 0,
|
|
440
|
+
headless: false,
|
|
441
|
+
userDataDir: join(
|
|
442
|
+
tmpdir(),
|
|
443
|
+
`monomind-browse-headed-${process.pid}-${randomUUID().slice(0, 8)}`,
|
|
444
|
+
),
|
|
445
|
+
});
|
|
271
446
|
} catch (err) {
|
|
272
447
|
// Headed launch failed (no display, locked down env). Restore headless and rethrow.
|
|
273
448
|
session.port = await browser.launchBrowser({ port, headless: true });
|
package/src/cli.ts
CHANGED
|
@@ -206,7 +206,9 @@ async function main(): Promise<void> {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
if (userArgs[0] === '--version' || userArgs[0] === '-V') {
|
|
209
|
-
|
|
209
|
+
// Resolved from the COMPILED location, `dist/src/cli.js` — `rootDir: "."`
|
|
210
|
+
// keeps the `src/` segment, so the manifest is two levels up, not one.
|
|
211
|
+
const pkg = _require('../../package.json') as { version: string };
|
|
210
212
|
console.log(pkg.version);
|
|
211
213
|
return;
|
|
212
214
|
}
|