@crouton-kit/capture 1.4.1 → 1.4.10
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 +178 -0
- package/bin/capture +31299 -7322
- package/package.json +3 -1
- package/src/capture.ts +21 -18
- package/src/cdp/args.ts +269 -49
- package/src/cdp/bridge/server.ts +6 -5
- package/src/cdp/bridge/spawn.ts +41 -97
- package/src/cdp/commands/bridge-serve.ts +11 -21
- package/src/cdp/commands/cdp.ts +44 -22
- package/src/cdp/commands/heap/census.ts +61 -0
- package/src/cdp/commands/heap/common.ts +77 -0
- package/src/cdp/commands/heap/diff.ts +92 -0
- package/src/cdp/commands/heap/index.ts +38 -0
- package/src/cdp/commands/heap/objects.ts +42 -0
- package/src/cdp/commands/heap/retainers.ts +52 -0
- package/src/cdp/commands/heap/snapshot.ts +94 -0
- package/src/cdp/commands/lib.ts +9 -17
- package/src/cdp/commands/lighthouse.ts +414 -0
- package/src/cdp/commands/measure/census.ts +2 -1
- package/src/cdp/commands/measure/check.ts +70 -16
- package/src/cdp/commands/measure/diff.ts +1 -2
- package/src/cdp/commands/measure/explain.ts +44 -7
- package/src/cdp/commands/measure/index.ts +24 -32
- package/src/cdp/commands/measure/map-ax.ts +2 -1
- package/src/cdp/commands/measure/map-focus.ts +2 -1
- package/src/cdp/commands/measure/map-layers.ts +5 -3
- package/src/cdp/commands/measure/map-scroll.ts +2 -1
- package/src/cdp/commands/measure/snap.ts +5 -4
- package/src/cdp/commands/measure/sweep.ts +16 -11
- package/src/cdp/commands/measure/text.ts +147 -0
- package/src/cdp/commands/motion/index.ts +10 -18
- package/src/cdp/commands/motion/jank.ts +13 -22
- package/src/cdp/commands/motion/mask.ts +10 -9
- package/src/cdp/commands/motion/rec.ts +230 -28
- package/src/cdp/commands/motion/timeline.ts +1 -1
- package/src/cdp/commands/page/click.ts +18 -8
- package/src/cdp/commands/page/elements.ts +43 -11
- package/src/cdp/commands/page/exec.ts +53 -18
- package/src/cdp/commands/page/index.ts +20 -20
- package/src/cdp/commands/page/inspect.ts +338 -0
- package/src/cdp/commands/page/navigate.ts +83 -38
- package/src/cdp/commands/page/repeat.ts +176 -0
- package/src/cdp/commands/page/scroll.ts +11 -6
- package/src/cdp/commands/page/shot.ts +68 -33
- package/src/cdp/commands/page/type.ts +7 -3
- package/src/cdp/commands/perf/index.ts +32 -0
- package/src/cdp/commands/perf/insights.ts +214 -0
- package/src/cdp/commands/perf/trace.ts +235 -0
- package/src/cdp/commands/perf/vitals.ts +76 -0
- package/src/cdp/commands/session/collectors.ts +54 -0
- package/src/cdp/commands/stub.ts +10 -0
- package/src/cdp/commands/tab/close.ts +2 -2
- package/src/cdp/commands/tab/index.ts +16 -23
- package/src/cdp/commands/tab/launch.ts +1 -1
- package/src/cdp/commands/tab/mock/index.ts +24 -0
- package/src/cdp/commands/tab/mock/lifecycle.ts +88 -0
- package/src/cdp/commands/tab/mock/start.ts +65 -0
- package/src/cdp/commands/tab/mock/stop.ts +39 -0
- package/src/cdp/commands/tab/network.ts +22 -13
- package/src/cdp/commands/tab/reset.ts +11 -12
- package/src/cdp/connection.ts +19 -39
- package/src/cdp/detect.ts +27 -7
- package/src/cdp/dispatch.ts +25 -4
- package/src/cdp/domains.ts +2 -2
- package/src/cdp/har-recorder.ts +0 -1
- package/src/cdp/heap-snapshot.ts +647 -0
- package/src/cdp/host/client.ts +22 -0
- package/src/cdp/host/collector.ts +74 -0
- package/src/cdp/host/collectors/heap.ts +79 -0
- package/src/cdp/host/collectors/intercept.ts +247 -0
- package/src/cdp/{recorder-bridge.ts → host/collectors/motion.ts} +388 -473
- package/src/cdp/host/collectors/trace.ts +108 -0
- package/src/cdp/host/core.ts +217 -0
- package/src/cdp/host/handle.ts +47 -0
- package/src/cdp/host/held-client.ts +91 -0
- package/src/cdp/host/kinds.ts +97 -0
- package/src/cdp/host/lifecycle.ts +123 -0
- package/src/cdp/host/server.ts +88 -0
- package/src/cdp/leaf-grammar.ts +2 -2
- package/src/cdp/measure/check.ts +181 -16
- package/src/cdp/measure/collectors/hittest.ts +16 -10
- package/src/cdp/measure/collectors/scroll.ts +15 -7
- package/src/cdp/measure/collectors/style-provenance.ts +11 -0
- package/src/cdp/measure/collectors/styles.ts +4 -4
- package/src/cdp/measure/explain.ts +14 -2
- package/src/cdp/measure/map-layers.ts +52 -8
- package/src/cdp/measure/snapshot.ts +5 -3
- package/src/cdp/measure/sweep.ts +29 -6
- package/src/cdp/measure/text.ts +227 -0
- package/src/cdp/motion/jank.ts +29 -83
- package/src/cdp/motion/mask.ts +18 -9
- package/src/cdp/motion/recorder.ts +86 -731
- package/src/cdp/scope-lock.ts +9 -9
- package/src/cdp/screenshot.ts +51 -21
- package/src/cdp/timing.ts +5 -5
- package/src/cdp/trace-analysis.ts +320 -0
- package/src/cdp/types.ts +31 -1
- package/src/cdp.ts +4 -6
- package/src/har-manager.ts +2 -4
- package/src/interact.ts +78 -25
- package/src/output/artifact.ts +49 -5
- package/src/output/render.ts +60 -9
- package/src/output/selection.ts +6 -0
- package/src/session/artifacts.ts +122 -12
- package/src/session/commands.ts +348 -113
- package/src/session/coordinator.ts +0 -93
- package/src/session/har-redact.ts +29 -23
- package/src/session-context.ts +13 -9
- package/src/cdp/bridge/protocol.ts +0 -224
- package/src/cdp/recorder-client.ts +0 -326
- package/src/cdp/tab-lock.ts +0 -132
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Client side of the recorder-bridge protocol (`./bridge/protocol.ts`'s
|
|
3
|
-
* `RecorderRequest`/`RecorderResponse` union) — the counterpart to
|
|
4
|
-
* `./bridge/client.ts`'s `sendBridgeRequest` for the plain held bridge, but
|
|
5
|
-
* typed against the recorder-mode wire shapes instead. Same convention: one
|
|
6
|
-
* Unix-socket connection per request, one response line, then the socket
|
|
7
|
-
* closes.
|
|
8
|
-
*
|
|
9
|
-
* This file also owns `RecorderHeldClient` — the `CDPClient`-compatible
|
|
10
|
-
* adapter `../connection.ts` hands to command leaves when a recording is
|
|
11
|
-
* active on the session's tab (see U14's routing decision there). A leaf
|
|
12
|
-
* that receives one behaves exactly as if it had opened its own tab
|
|
13
|
-
* websocket: `.send()` round-trips a CDP call through the recorder's held
|
|
14
|
-
* connection, `.close()` is a no-op (the recorder owns the connection until
|
|
15
|
-
* `motion rec --stop`, not the leaf command that borrowed it), and
|
|
16
|
-
* `.on()`/`.onDisconnect()` are documented no-ops — a routed command's own
|
|
17
|
-
* `ConsoleRecorder`/`HARRecorder` would only ever see zero events through
|
|
18
|
-
* this adapter (nothing pushes unsolicited events back over the
|
|
19
|
-
* one-request-one-response socket), so `../connection.ts` skips wiring them
|
|
20
|
-
* entirely when routed, rather than silently reporting an empty console/HAR
|
|
21
|
-
* summary as if it were real. HTTP evidence for a routed action is NOT lost:
|
|
22
|
-
* the recorder bridge holds the one streaming `HARRecorder` on its held tab
|
|
23
|
-
* connection, appending each completed entry into the owning session's live
|
|
24
|
-
* HAR continuously, and `../connection.ts`'s action wrapper awaits this
|
|
25
|
-
* adapter's `flushHar()` health barrier before claiming success, so traffic
|
|
26
|
-
* completed by action+settle time is durably in the session HAR when the
|
|
27
|
-
* command returns.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import * as net from 'net';
|
|
31
|
-
import {
|
|
32
|
-
type RecorderRequest,
|
|
33
|
-
type RecorderResponse,
|
|
34
|
-
type RecStartResponseOk,
|
|
35
|
-
type RecStopResponseOk,
|
|
36
|
-
} from './bridge/protocol.js';
|
|
37
|
-
|
|
38
|
-
let reqCounter = 0;
|
|
39
|
-
|
|
40
|
-
/** Raw one-shot request/response round trip over a recorder bridge's socket. */
|
|
41
|
-
export function sendRecorderRequest(
|
|
42
|
-
socketPath: string,
|
|
43
|
-
req: Omit<RecorderRequest, 'reqId'>,
|
|
44
|
-
): Promise<RecorderResponse> {
|
|
45
|
-
const timeoutMsField = (req as { timeoutMs?: number }).timeoutMs;
|
|
46
|
-
const wireTimeoutMs = (timeoutMsField ?? 10000) + 5000;
|
|
47
|
-
|
|
48
|
-
return new Promise((resolve, reject) => {
|
|
49
|
-
const reqId = ++reqCounter;
|
|
50
|
-
const socket = net.createConnection(socketPath);
|
|
51
|
-
let buffer = '';
|
|
52
|
-
|
|
53
|
-
const timer = setTimeout(() => {
|
|
54
|
-
socket.destroy();
|
|
55
|
-
reject(new Error(`Recorder request timed out after ${wireTimeoutMs}ms (socket: ${socketPath})`));
|
|
56
|
-
}, wireTimeoutMs);
|
|
57
|
-
|
|
58
|
-
socket.on('connect', () => {
|
|
59
|
-
socket.write(JSON.stringify({ ...req, reqId } as RecorderRequest) + '\n');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
socket.on('data', (chunk) => {
|
|
63
|
-
buffer += chunk.toString('utf-8');
|
|
64
|
-
const idx = buffer.indexOf('\n');
|
|
65
|
-
if (idx < 0) return;
|
|
66
|
-
clearTimeout(timer);
|
|
67
|
-
const line = buffer.slice(0, idx);
|
|
68
|
-
socket.end();
|
|
69
|
-
try {
|
|
70
|
-
resolve(JSON.parse(line) as RecorderResponse);
|
|
71
|
-
} catch (err) {
|
|
72
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
socket.on('error', (err) => {
|
|
77
|
-
clearTimeout(timer);
|
|
78
|
-
reject(err);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Arms the recorder (`rec-start`). `nonce` is the recording's control-socket
|
|
84
|
-
* admission token (see `protocol.ts`'s `RecStartRequest`); every request must
|
|
85
|
-
* carry it. Throws with the recorder's own error message on failure. */
|
|
86
|
-
export async function requestRecStart(socketPath: string, nonce: string): Promise<RecStartResponseOk> {
|
|
87
|
-
const resp = await sendRecorderRequest(socketPath, { type: 'rec-start', nonce });
|
|
88
|
-
if (!resp.ok) throw new Error(`rec-start failed: ${resp.error}`);
|
|
89
|
-
return resp;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Thrown by `requestRecStop` when the bridge actually answered with an
|
|
94
|
-
* authenticated `ok:false` response — an explicit, terminal recorder-stop
|
|
95
|
-
* failure (fatal HAR drain, or any other authoritative rejection the bridge
|
|
96
|
-
* itself produced), as opposed to a transport/no-response failure (socket
|
|
97
|
-
* connect/timeout error), which `sendRecorderRequest` still rejects with a
|
|
98
|
-
* plain `Error`. Callers that only read `.message` see the exact same
|
|
99
|
-
* `rec-stop failed: ${error}` text as before this type existed; callers that
|
|
100
|
-
* need to distinguish "the bridge answered and refused" from "the bridge
|
|
101
|
-
* never answered" (`../motion/recorder.ts`'s terminal-vs-transport
|
|
102
|
-
* classification) narrow with `instanceof RecStopBridgeFailure` and read
|
|
103
|
-
* `.responseError` for the bridge's raw, unwrapped error string.
|
|
104
|
-
*/
|
|
105
|
-
export class RecStopBridgeFailure extends Error {
|
|
106
|
-
readonly responseError: string;
|
|
107
|
-
constructor(responseError: string) {
|
|
108
|
-
super(`rec-stop failed: ${responseError}`);
|
|
109
|
-
this.name = 'RecStopBridgeFailure';
|
|
110
|
-
this.responseError = responseError;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Finalizes the recorder (`rec-stop`). Throws `RecStopBridgeFailure` (preserving the recorder's own error message) when the bridge answers `ok:false`; a transport/no-response failure still throws a plain `Error`. */
|
|
115
|
-
export async function requestRecStop(socketPath: string, nonce: string): Promise<RecStopResponseOk> {
|
|
116
|
-
const resp = await sendRecorderRequest(socketPath, { type: 'rec-stop', nonce });
|
|
117
|
-
if (!resp.ok) throw new RecStopBridgeFailure(resp.error);
|
|
118
|
-
return resp;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** CDP method names the recorder bridge's `mark` mechanism is meant for —
|
|
122
|
-
* see `../recorder-bridge.ts`'s `handleCdp()`: a `mark`-bearing request is
|
|
123
|
-
* bracketed with two `performance.now()` reads and records a labeled
|
|
124
|
-
* input-landmark entry straight to `events.jsonl` (host-side only — the
|
|
125
|
-
* label never touches the page, see `../timing.ts`'s
|
|
126
|
-
* `withDocumentPerformanceNow`), so it must only be set on the CDP calls that
|
|
127
|
-
* actually dispatch a distinct, landmark-worthy ACTION, not on every
|
|
128
|
-
* incidental call a higher-level helper (`resolveLiveTarget`, `focusAndType`, ...)
|
|
129
|
-
* happens to make while resolving a selector/name (`Accessibility.*`,
|
|
130
|
-
* `DOM.*`, `Runtime.evaluate`, `Page.captureScreenshot`, ...). Every
|
|
131
|
-
* `Input.dispatch*` method (mouse/key/touch) is covered by the prefix;
|
|
132
|
-
* `Input.insertText` is NOT a `dispatch*` method but IS the actual mutator
|
|
133
|
-
* `../../interact.ts`'s `typeText()`/`focusAndType()` issue for `capture
|
|
134
|
-
* page type`, so it is listed explicitly: routed typing needs one input
|
|
135
|
-
* landmark in `events.jsonl` for `motion response` to anchor. `Page.navigate`
|
|
136
|
-
* is likewise the action landmark for `capture page navigate`; the
|
|
137
|
-
* recorder-routed path in `../commands/page/navigate.ts` sends it through
|
|
138
|
-
* this client just like click and type. */
|
|
139
|
-
const MARKABLE_EXACT_METHODS = new Set(['Input.insertText', 'Page.navigate']);
|
|
140
|
-
|
|
141
|
-
function isMarkableActionMethod(method: string, params: Record<string, unknown>): boolean {
|
|
142
|
-
if (MARKABLE_EXACT_METHODS.has(method)) return true;
|
|
143
|
-
// A logical input may require several low-level dispatches. Mark its
|
|
144
|
-
// initiating edge only: e.g. clickResolved emits press + release, which is
|
|
145
|
-
// one landmark rather than two identically-labelled observations.
|
|
146
|
-
if (method === 'Input.dispatchMouseEvent') return params.type === 'mousePressed';
|
|
147
|
-
if (method === 'Input.dispatchKeyEvent') return params.type === 'keyDown' || params.type === 'rawKeyDown';
|
|
148
|
-
if (method === 'Input.dispatchTouchEvent') return params.type === 'touchStart';
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export interface RecorderHeldClientOptions {
|
|
153
|
-
/** The active recording's socket path (`recorderSocketPath(recDir)`). */
|
|
154
|
-
socketPath: string;
|
|
155
|
-
/** The recording's control-socket admission token, read from the persisted
|
|
156
|
-
* recorder handle (`recorder.json.nonce`) — required on every request this
|
|
157
|
-
* adapter sends; the bridge rejects anything without it. */
|
|
158
|
-
nonce: string;
|
|
159
|
-
/** Label attached to every marked (`Input.dispatch*`) call from this
|
|
160
|
-
* client instance — one per routed capture command invocation, e.g.
|
|
161
|
-
* `click:Send` or `type:ax:Message`, matching the labeled input landmark
|
|
162
|
-
* shape in `events.jsonl` without recording typed content. */
|
|
163
|
-
actionLabel: string;
|
|
164
|
-
timeoutMs?: number;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* `CDPClient`-compatible adapter over a live recorder connection. Structural
|
|
169
|
-
* subset (`send`/`on`/`onDisconnect`/`close`) rather than a subclass — see
|
|
170
|
-
* `../connection.ts`'s `connectToActiveRecorder()`, which is the only
|
|
171
|
-
* caller, and which casts the result to `CDPClient` the same documented way
|
|
172
|
-
* `../recorder-bridge.ts`'s `asCDPClient()` does, so every existing command
|
|
173
|
-
* leaf keeps working against the same type it always has.
|
|
174
|
-
*/
|
|
175
|
-
export class RecorderHeldClient {
|
|
176
|
-
private readonly socketPath: string;
|
|
177
|
-
private readonly nonce: string;
|
|
178
|
-
private readonly actionLabel: string;
|
|
179
|
-
private readonly defaultTimeoutMs: number;
|
|
180
|
-
private suppressNextMousePressMark = false;
|
|
181
|
-
|
|
182
|
-
constructor(opts: RecorderHeldClientOptions) {
|
|
183
|
-
this.socketPath = opts.socketPath;
|
|
184
|
-
this.nonce = opts.nonce;
|
|
185
|
-
this.actionLabel = opts.actionLabel;
|
|
186
|
-
this.defaultTimeoutMs = opts.timeoutMs ?? 60000;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** Suppresses the focus click's initiating edge so a routed `type --into`
|
|
190
|
-
* has one landmark on its actual text insertion. */
|
|
191
|
-
suppressNextFocusClickMark(): void {
|
|
192
|
-
this.suppressNextMousePressMark = true;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async send(
|
|
196
|
-
method: string,
|
|
197
|
-
params: Record<string, unknown> = {},
|
|
198
|
-
timeoutMs?: number,
|
|
199
|
-
_sessionId?: string,
|
|
200
|
-
): Promise<unknown> {
|
|
201
|
-
const { result } = await this.dispatch(method, params, undefined, timeoutMs);
|
|
202
|
-
return result;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** Marked lane for a mutating call the auto-markable set above doesn't
|
|
206
|
-
* cover — `Runtime.callFunctionOn` in `../../interact.ts`'s
|
|
207
|
-
* `scrollResolved`, whose caller supplies the landmark label explicitly
|
|
208
|
-
* (`LiveClient.sendMarked`). Rides the same bridge `mark` mechanism as
|
|
209
|
-
* every auto-marked call. */
|
|
210
|
-
async sendMarked(method: string, params: Record<string, unknown>, mark: string): Promise<unknown> {
|
|
211
|
-
const resp = await sendRecorderRequest(this.socketPath, {
|
|
212
|
-
type: 'cdp',
|
|
213
|
-
nonce: this.nonce,
|
|
214
|
-
method,
|
|
215
|
-
params,
|
|
216
|
-
mark,
|
|
217
|
-
timeoutMs: this.defaultTimeoutMs,
|
|
218
|
-
});
|
|
219
|
-
if (!resp.ok) throw new Error(`recorder-routed marked CDP call "${method}" failed: ${resp.error}`);
|
|
220
|
-
return resp.result;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Blocks on the recorder bridge's own event broker for the next
|
|
225
|
-
* occurrence of `eventName`, via a wait-event-ONLY request (`method`
|
|
226
|
-
* omitted — see `RecCdpWaitEventRequest`). This is the real event-wait
|
|
227
|
-
* surface for adapter users, distinct from `.on()` above. `capture cdp
|
|
228
|
-
* --wait-event` is the primary caller (`../commands/cdp.ts`) when no
|
|
229
|
-
* method is also being dispatched.
|
|
230
|
-
*/
|
|
231
|
-
async waitEvent(eventName: string, timeoutMs?: number): Promise<unknown> {
|
|
232
|
-
const resp = await sendRecorderRequest(this.socketPath, {
|
|
233
|
-
type: 'cdp',
|
|
234
|
-
nonce: this.nonce,
|
|
235
|
-
waitEvent: eventName,
|
|
236
|
-
timeoutMs: timeoutMs ?? this.defaultTimeoutMs,
|
|
237
|
-
});
|
|
238
|
-
if (!resp.ok) {
|
|
239
|
-
throw new Error(`recorder-routed wait-event "${eventName}" failed: ${resp.error}`);
|
|
240
|
-
}
|
|
241
|
-
return resp.event;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Dispatches `method` and, when `waitEvent` is given, arms the wait for it
|
|
246
|
-
* in the SAME `sendRecorderRequest` call — one unix-socket connection, one
|
|
247
|
-
* request line carrying `method`, `params`, `mark`, and (optionally)
|
|
248
|
-
* `waitEvent` together. The bridge's own `RecorderSession.handleCdp()`
|
|
249
|
-
* registers the event wait BEFORE dispatching the CDP call within that
|
|
250
|
-
* one request, so this is race-free against a fast action-caused event —
|
|
251
|
-
* unlike `.send()` followed by `.waitEvent()`, which are two SEPARATE
|
|
252
|
-
* connections/requests with no ordering guarantee between them. `.send()`
|
|
253
|
-
* above is just this method with `waitEvent` omitted; every caller that
|
|
254
|
-
* needs to dispatch a method and observe an event it may itself trigger
|
|
255
|
-
* should call this directly instead.
|
|
256
|
-
*
|
|
257
|
-
* When `method` and `waitEvent` are BOTH set, a successful dispatch is
|
|
258
|
-
* preserved even if the paired wait times out: the bridge's `handleCdp`
|
|
259
|
-
* returns the method `result` plus a `waitOutcome` of `'observed'` (the
|
|
260
|
-
* event fired within the deadline, `event` populated) or `'bounded-timeout'`
|
|
261
|
-
* (the event did not fire in time — the method still succeeded). Only a
|
|
262
|
-
* FAILURE of the dispatched method itself yields `ok:false`. A wait-event-
|
|
263
|
-
* only request (`method` omitted) keeps the older semantics: a wait timeout
|
|
264
|
-
* is itself the failure and surfaces as `ok:false`, with no `waitOutcome`.
|
|
265
|
-
*/
|
|
266
|
-
async dispatch(
|
|
267
|
-
method: string,
|
|
268
|
-
params: Record<string, unknown> = {},
|
|
269
|
-
waitEvent?: string,
|
|
270
|
-
timeoutMs?: number,
|
|
271
|
-
): Promise<{ result: unknown; event?: unknown; waitOutcome?: 'observed' | 'bounded-timeout' }> {
|
|
272
|
-
const suppressMousePress = method === 'Input.dispatchMouseEvent' && params.type === 'mousePressed' && this.suppressNextMousePressMark;
|
|
273
|
-
if (suppressMousePress) this.suppressNextMousePressMark = false;
|
|
274
|
-
const mark = !suppressMousePress && isMarkableActionMethod(method, params) ? this.actionLabel : undefined;
|
|
275
|
-
const resp = await sendRecorderRequest(this.socketPath, {
|
|
276
|
-
type: 'cdp',
|
|
277
|
-
nonce: this.nonce,
|
|
278
|
-
method,
|
|
279
|
-
params,
|
|
280
|
-
mark,
|
|
281
|
-
waitEvent,
|
|
282
|
-
timeoutMs: timeoutMs ?? this.defaultTimeoutMs,
|
|
283
|
-
});
|
|
284
|
-
if (!resp.ok) {
|
|
285
|
-
throw new Error(
|
|
286
|
-
`recorder-routed CDP call "${method}"${waitEvent ? ` with wait-event "${waitEvent}"` : ''} failed: ${resp.error}`,
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
return { result: resp.result, event: resp.event, waitOutcome: resp.waitOutcome };
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* The action-return HAR flush/health barrier (`har-flush` — see
|
|
294
|
-
* `./bridge/protocol.ts`'s `RecHarFlushRequest`). Resolves once every HAR
|
|
295
|
-
* entry/body/append the recorder's streaming collector had completed at
|
|
296
|
-
* request time is durably in the owning session's live HAR and no fatal
|
|
297
|
-
* store error is latched. Non-attributing: success carries no counts and
|
|
298
|
-
* claims nothing about which entries this action caused. Throws when the
|
|
299
|
-
* bridge answers `ok:false` (a latched fatal store failure, or the terminal
|
|
300
|
-
* rec-stop already owns finalization) or on a transport failure.
|
|
301
|
-
*/
|
|
302
|
-
async flushHar(): Promise<void> {
|
|
303
|
-
const resp = await sendRecorderRequest(this.socketPath, { type: 'har-flush', nonce: this.nonce });
|
|
304
|
-
if (!resp.ok) throw new Error(`recorder har-flush failed: ${resp.error}`);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/** Documented no-op — see this file's header comment. */
|
|
308
|
-
on(_event: string, _handler: (params: unknown) => void): void {
|
|
309
|
-
// Intentionally does nothing: nothing pushes unsolicited events back
|
|
310
|
-
// over the recorder's one-request-one-response socket.
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/** Documented no-op — the recorder owns the connection's lifetime, not this borrowed handle. */
|
|
314
|
-
onDisconnect(_handler: () => void): void {}
|
|
315
|
-
|
|
316
|
-
/** Documented no-op — closing would tear down the leaf's OWN (nonexistent) socket, never the recorder's held connection. `motion rec --stop` is what ends the recording. */
|
|
317
|
-
close(): void {}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/** Type guard used by `../connection.ts`'s `withConnection()` to skip wiring
|
|
321
|
-
* `ConsoleRecorder`/`HARRecorder` (which would otherwise silently report an
|
|
322
|
-
* empty summary — see this file's header) when the command's client is
|
|
323
|
-
* actually a recorder-routed adapter underneath its `CDPClient` cast. */
|
|
324
|
-
export function isRecorderHeldClient(client: unknown): client is RecorderHeldClient {
|
|
325
|
-
return client instanceof RecorderHeldClient;
|
|
326
|
-
}
|
package/src/cdp/tab-lock.ts
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as os from 'os';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
|
|
5
|
-
const LOCK_DIR = os.tmpdir();
|
|
6
|
-
const LOCK_STALE_MS = 120_000;
|
|
7
|
-
const LOCK_TIMEOUT_MS = 60_000;
|
|
8
|
-
const LOCK_POLL_BASE_MS = 200;
|
|
9
|
-
|
|
10
|
-
function lockPath(tabId: string): string {
|
|
11
|
-
return path.join(LOCK_DIR, `capture-cdp-tab-${tabId}.lock`);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function isPidAlive(pid: number): boolean {
|
|
15
|
-
try {
|
|
16
|
-
process.kill(pid, 0);
|
|
17
|
-
return true;
|
|
18
|
-
} catch {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function isLockStale(filePath: string): boolean {
|
|
24
|
-
try {
|
|
25
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
26
|
-
const { pid, ts } = JSON.parse(content) as { pid: number; ts: number };
|
|
27
|
-
if (!isPidAlive(pid)) return true;
|
|
28
|
-
return Date.now() - ts > LOCK_STALE_MS;
|
|
29
|
-
} catch {
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Track locks held by this process for cleanup on exit
|
|
35
|
-
const heldLocks = new Set<string>();
|
|
36
|
-
let cleanupRegistered = false;
|
|
37
|
-
|
|
38
|
-
function registerCleanup(): void {
|
|
39
|
-
if (cleanupRegistered) return;
|
|
40
|
-
cleanupRegistered = true;
|
|
41
|
-
|
|
42
|
-
const cleanup = () => {
|
|
43
|
-
for (const file of heldLocks) {
|
|
44
|
-
try {
|
|
45
|
-
fs.unlinkSync(file);
|
|
46
|
-
} catch {
|
|
47
|
-
// Best-effort
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
heldLocks.clear();
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
process.on('exit', cleanup);
|
|
54
|
-
process.on('SIGINT', () => {
|
|
55
|
-
cleanup();
|
|
56
|
-
process.exit(130);
|
|
57
|
-
});
|
|
58
|
-
process.on('SIGTERM', () => {
|
|
59
|
-
cleanup();
|
|
60
|
-
process.exit(143);
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export async function acquireTabLock(tabId: string): Promise<void> {
|
|
65
|
-
registerCleanup();
|
|
66
|
-
const file = lockPath(tabId);
|
|
67
|
-
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
68
|
-
let delay = LOCK_POLL_BASE_MS;
|
|
69
|
-
|
|
70
|
-
while (true) {
|
|
71
|
-
try {
|
|
72
|
-
const fd = fs.openSync(file, 'wx');
|
|
73
|
-
fs.writeSync(fd, JSON.stringify({ pid: process.pid, ts: Date.now() }));
|
|
74
|
-
fs.closeSync(fd);
|
|
75
|
-
heldLocks.add(file);
|
|
76
|
-
return;
|
|
77
|
-
} catch (err: unknown) {
|
|
78
|
-
if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (isLockStale(file)) {
|
|
82
|
-
try {
|
|
83
|
-
fs.unlinkSync(file);
|
|
84
|
-
} catch {
|
|
85
|
-
// Another process may have already removed it
|
|
86
|
-
}
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (Date.now() >= deadline) {
|
|
91
|
-
throw new Error(
|
|
92
|
-
`Timed out waiting for CDP tab lock (${tabId}). ` +
|
|
93
|
-
`Another process may be using this tab. Lock file: ${file}`,
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
98
|
-
delay = Math.min(delay * 2, 2000);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function isTabLocked(tabId: string): boolean {
|
|
103
|
-
const file = lockPath(tabId);
|
|
104
|
-
try {
|
|
105
|
-
fs.accessSync(file);
|
|
106
|
-
return !isLockStale(file);
|
|
107
|
-
} catch {
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function releaseTabLock(tabId: string): void {
|
|
113
|
-
const file = lockPath(tabId);
|
|
114
|
-
heldLocks.delete(file);
|
|
115
|
-
try {
|
|
116
|
-
fs.unlinkSync(file);
|
|
117
|
-
} catch {
|
|
118
|
-
// Already removed — fine
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export async function withTabLock<T>(
|
|
123
|
-
tabId: string,
|
|
124
|
-
fn: () => Promise<T>,
|
|
125
|
-
): Promise<T> {
|
|
126
|
-
await acquireTabLock(tabId);
|
|
127
|
-
try {
|
|
128
|
-
return await fn();
|
|
129
|
-
} finally {
|
|
130
|
-
releaseTabLock(tabId);
|
|
131
|
-
}
|
|
132
|
-
}
|