cdp-toolkit 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +18 -4
- package/dist/bidi/mux-daemon.js +3773 -0
- package/dist/cli.js +6457 -5690
- package/dist/index.js +275 -210
- package/dist/mcp.js +8128 -7361
- package/package.json +2 -1
- package/src/bidi/driver.ts +329 -30
- package/src/bidi/mux-daemon.ts +122 -0
- package/src/bidi/mux.ts +610 -0
- package/src/bidi/session-lease.ts +95 -3
- package/src/manifest.ts +1 -0
- package/src/version.ts +1 -1
|
@@ -35,8 +35,34 @@
|
|
|
35
35
|
* ATOMICITY. Claims are exclusive creates (writeFile flag "wx"), exactly like
|
|
36
36
|
* ../leases.ts claimLease, so "is the slot free, and if so take it" is atomic
|
|
37
37
|
* against a racing process for free, with no registry lock.
|
|
38
|
+
*
|
|
39
|
+
* THE MUX: WHY A LIVE HOLDER IS NO LONGER A DEAD END. Everything above still
|
|
40
|
+
* holds — one process owns Firefox's single BiDi session — but the holder now
|
|
41
|
+
* also hosts a loopback BiDi multiplexer (./mux.ts) over that one real session
|
|
42
|
+
* and ADVERTISES its ws endpoint in this record (`muxEndpoint`). Two consequences
|
|
43
|
+
* for this module, and nothing else changes:
|
|
44
|
+
*
|
|
45
|
+
* - THE HOLDER advertises via advertiseMux() once its mux is listening. That is
|
|
46
|
+
* a rewrite of its OWN record, guarded by the same (pid, createdAt) ownership
|
|
47
|
+
* test release uses, and written atomically (tmp + rename) so a concurrent
|
|
48
|
+
* reader never sees a half-written record. Advertising is best-effort: a
|
|
49
|
+
* refusal (`advertised:false`) means the record is no longer ours and the
|
|
50
|
+
* holder simply runs alone.
|
|
51
|
+
* - A JOINER DOES NOT WAIT. When acquire finds a LIVE holder that carries a
|
|
52
|
+
* muxEndpoint, there is nothing to wait FOR: the slot will not free up (the
|
|
53
|
+
* holder is healthy) and it does not need to, because the mux serves that one
|
|
54
|
+
* session to as many processes as ask. So acquire returns { joinMux }
|
|
55
|
+
* immediately, with NO handle and nothing written to disk — the joiner holds
|
|
56
|
+
* no slot and must never release one. The record is re-read on every poll
|
|
57
|
+
* iteration, so a holder that took the slot a moment ago and advertises a beat
|
|
58
|
+
* later is joined as soon as it does, inside the existing wait window.
|
|
59
|
+
*
|
|
60
|
+
* A live holder with NO muxEndpoint (a foreign client, an older cdp-toolkit, or
|
|
61
|
+
* one running with CDP_FIREFOX_MUX=off) is still plain BUSY and still throws
|
|
62
|
+
* SessionSlotBusyError at the deadline — exactly today's behavior. CDP_FIREFOX_MUX=off
|
|
63
|
+
* makes acquire ignore muxEndpoint entirely, which is the opt-out.
|
|
38
64
|
*/
|
|
39
|
-
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
65
|
+
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
40
66
|
import { join } from "node:path";
|
|
41
67
|
import { isPidAlive, leaseDir } from "../leases.ts";
|
|
42
68
|
|
|
@@ -55,6 +81,12 @@ export interface SessionSlotRecord {
|
|
|
55
81
|
createdAt: number;
|
|
56
82
|
/** Optional Marionette control port bound to this Firefox, when known. */
|
|
57
83
|
marionettePort?: number;
|
|
84
|
+
/**
|
|
85
|
+
* Loopback ws endpoint of the BiDi multiplexer this holder hosts over the one
|
|
86
|
+
* real session (see ./mux.ts and the header). Absent means "this holder serves
|
|
87
|
+
* nobody but itself", which is what every pre-mux client's record looks like.
|
|
88
|
+
*/
|
|
89
|
+
muxEndpoint?: string;
|
|
58
90
|
}
|
|
59
91
|
|
|
60
92
|
/**
|
|
@@ -68,7 +100,11 @@ export interface SessionSlotHandle {
|
|
|
68
100
|
}
|
|
69
101
|
|
|
70
102
|
export interface AcquireResult {
|
|
71
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Present iff WE took the slot. Absent on the joinMux path: a joiner owns no
|
|
105
|
+
* slot, wrote no record, and must never release one.
|
|
106
|
+
*/
|
|
107
|
+
handle?: SessionSlotHandle;
|
|
72
108
|
/**
|
|
73
109
|
* Set only when we STOLE a dead holder's slot. Its presence tells the caller
|
|
74
110
|
* that Firefox may still believe an orphaned BiDi session is active, so the
|
|
@@ -77,6 +113,12 @@ export interface AcquireResult {
|
|
|
77
113
|
* sessions". Absent on a clean first acquire.
|
|
78
114
|
*/
|
|
79
115
|
staleHolder?: SessionSlotRecord;
|
|
116
|
+
/**
|
|
117
|
+
* Set instead of `handle` when a LIVE holder advertises a BiDi multiplexer.
|
|
118
|
+
* The caller dials `endpoint` exactly as it would dial Firefox; `holder` is the
|
|
119
|
+
* record it came from, for error reporting. Nothing was written to disk.
|
|
120
|
+
*/
|
|
121
|
+
joinMux?: { endpoint: string; holder: SessionSlotRecord };
|
|
80
122
|
}
|
|
81
123
|
|
|
82
124
|
export interface AcquireOptions {
|
|
@@ -105,6 +147,16 @@ export function sessionSlotFile(endpoint: string): string {
|
|
|
105
147
|
return join(leaseDir(), `ff-session-${safeEndpoint(endpoint)}.json`);
|
|
106
148
|
}
|
|
107
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Where the detached mux DAEMON (./mux-daemon.ts) appends its one-line diagnostics for this
|
|
152
|
+
* endpoint. It is spawned with stdio:"ignore" (no console at all), so this file is the ONLY
|
|
153
|
+
* record of why a daemon refused to start; the driver names it in the error it raises when a
|
|
154
|
+
* daemon it spawned never advertised. Sibling of the slot file, same safeEndpoint key.
|
|
155
|
+
*/
|
|
156
|
+
export function muxDaemonLogFile(endpoint: string): string {
|
|
157
|
+
return join(leaseDir(), `ff-mux-${safeEndpoint(endpoint)}.log`);
|
|
158
|
+
}
|
|
159
|
+
|
|
108
160
|
/** Read per call (like leaseDir/leaseTtlMs), so a test can redirect it. A value
|
|
109
161
|
* that is not a finite number > 0 falls back to the default. */
|
|
110
162
|
export function sessionWaitMs(): number {
|
|
@@ -236,7 +288,16 @@ export async function acquireSessionSlot(endpoint: string, opts: AcquireOptions)
|
|
|
236
288
|
}
|
|
237
289
|
}
|
|
238
290
|
|
|
239
|
-
// 4. LIVE holder
|
|
291
|
+
// 4. LIVE holder. If it advertises a mux, JOIN it instead of waiting: the slot
|
|
292
|
+
// is never going to free up, and it does not have to — the holder's mux serves
|
|
293
|
+
// the one real session to every joiner. Return immediately, writing nothing.
|
|
294
|
+
// (The re-read at step 2 happens every iteration, so a holder that advertises a
|
|
295
|
+
// beat after taking the slot is picked up on the next poll.) CDP_FIREFOX_MUX=off
|
|
296
|
+
// opts out and restores the pre-mux busy-wait exactly.
|
|
297
|
+
if (typeof holder.muxEndpoint === "string" && holder.muxEndpoint.length > 0 && process.env.CDP_FIREFOX_MUX !== "off") {
|
|
298
|
+
return { joinMux: { endpoint: holder.muxEndpoint, holder } };
|
|
299
|
+
}
|
|
300
|
+
// No mux: legitimate contention. Poll until the deadline, then fail.
|
|
240
301
|
lastLiveHolder = holder;
|
|
241
302
|
const remaining = deadline - Date.now();
|
|
242
303
|
if (remaining <= 0) throw new SessionSlotBusyError(endpoint, holder);
|
|
@@ -244,6 +305,37 @@ export async function acquireSessionSlot(endpoint: string, opts: AcquireOptions)
|
|
|
244
305
|
}
|
|
245
306
|
}
|
|
246
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Publish this holder's BiDi multiplexer endpoint into its OWN slot record, so a
|
|
310
|
+
* contending process joins the mux instead of waiting out the slot.
|
|
311
|
+
*
|
|
312
|
+
* Ownership-guarded by the same (pid, createdAt) test releaseSessionSlot uses: if
|
|
313
|
+
* the record on disk is missing or belongs to someone else (we were stolen from
|
|
314
|
+
* as a dead-looking holder, or we already released), we refuse with
|
|
315
|
+
* { advertised: false } and leave the file byte-for-byte untouched rather than
|
|
316
|
+
* stamping our mux onto a stranger's claim.
|
|
317
|
+
*
|
|
318
|
+
* Atomic: the merged record is written to `<file>.tmp-<pid>` and renamed over the
|
|
319
|
+
* original, so a concurrent acquire's read sees either the old record or the new
|
|
320
|
+
* one, never a truncated one. Every other field is preserved.
|
|
321
|
+
*/
|
|
322
|
+
export async function advertiseMux(handle: SessionSlotHandle, muxEndpoint: string): Promise<{ advertised: boolean }> {
|
|
323
|
+
const current = await readSessionSlot(handle.endpoint);
|
|
324
|
+
if (!current) return { advertised: false };
|
|
325
|
+
if (current.pid !== process.pid || current.createdAt !== handle.createdAt) return { advertised: false };
|
|
326
|
+
const file = sessionSlotFile(handle.endpoint);
|
|
327
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
328
|
+
const next: SessionSlotRecord = { ...current, muxEndpoint };
|
|
329
|
+
try {
|
|
330
|
+
await writeFile(tmp, JSON.stringify(next), "utf8");
|
|
331
|
+
await rename(tmp, file);
|
|
332
|
+
} catch {
|
|
333
|
+
await unlink(tmp).catch(() => undefined);
|
|
334
|
+
return { advertised: false };
|
|
335
|
+
}
|
|
336
|
+
return { advertised: true };
|
|
337
|
+
}
|
|
338
|
+
|
|
247
339
|
/**
|
|
248
340
|
* Release the slot, but ONLY if the record on disk is still the one we wrote.
|
|
249
341
|
*
|
package/src/manifest.ts
CHANGED
package/src/version.ts
CHANGED