@pdpp/local-collector 0.0.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/README.md +48 -0
- package/dist/local-collector/bin/pdpp-local-collector.js +347 -0
- package/dist/local-collector/src/errors.d.ts +12 -0
- package/dist/local-collector/src/errors.js +20 -0
- package/dist/local-collector/src/runner.d.ts +16 -0
- package/dist/local-collector/src/runner.js +59 -0
- package/dist/polyfill-connectors/connectors/claude_code/index.js +806 -0
- package/dist/polyfill-connectors/connectors/claude_code/parsers.js +224 -0
- package/dist/polyfill-connectors/connectors/claude_code/schemas.js +120 -0
- package/dist/polyfill-connectors/connectors/claude_code/types.js +1 -0
- package/dist/polyfill-connectors/connectors/codex/index.js +880 -0
- package/dist/polyfill-connectors/connectors/codex/parsers.js +159 -0
- package/dist/polyfill-connectors/connectors/codex/schemas.js +118 -0
- package/dist/polyfill-connectors/connectors/codex/types.js +1 -0
- package/dist/polyfill-connectors/src/auth.js +76 -0
- package/dist/polyfill-connectors/src/browser-handoff.js +197 -0
- package/dist/polyfill-connectors/src/collector-protocol.d.ts +2 -0
- package/dist/polyfill-connectors/src/collector-protocol.js +2 -0
- package/dist/polyfill-connectors/src/collector-runner.d.ts +139 -0
- package/dist/polyfill-connectors/src/collector-runner.js +1084 -0
- package/dist/polyfill-connectors/src/connector-runtime-protocol.d.ts +191 -0
- package/dist/polyfill-connectors/src/connector-runtime-protocol.js +1 -0
- package/dist/polyfill-connectors/src/connector-runtime.js +879 -0
- package/dist/polyfill-connectors/src/fixture-capture.js +237 -0
- package/dist/polyfill-connectors/src/is-main-module.d.ts +1 -0
- package/dist/polyfill-connectors/src/is-main-module.js +17 -0
- package/dist/polyfill-connectors/src/local-device-client.d.ts +126 -0
- package/dist/polyfill-connectors/src/local-device-client.js +132 -0
- package/dist/polyfill-connectors/src/local-device-envelope.d.ts +26 -0
- package/dist/polyfill-connectors/src/local-device-envelope.js +43 -0
- package/dist/polyfill-connectors/src/local-device-outbox.d.ts +115 -0
- package/dist/polyfill-connectors/src/local-device-outbox.js +509 -0
- package/dist/polyfill-connectors/src/local-device-queue.d.ts +34 -0
- package/dist/polyfill-connectors/src/local-device-queue.js +133 -0
- package/dist/polyfill-connectors/src/local-source-inventory.js +119 -0
- package/dist/polyfill-connectors/src/pdpp-safe-text.js +13 -0
- package/dist/polyfill-connectors/src/runner/index.d.ts +11 -0
- package/dist/polyfill-connectors/src/runner/index.js +10 -0
- package/dist/polyfill-connectors/src/runtime-capabilities.d.ts +40 -0
- package/dist/polyfill-connectors/src/runtime-capabilities.js +59 -0
- package/dist/polyfill-connectors/src/safe-emit.d.ts +3 -0
- package/dist/polyfill-connectors/src/safe-emit.js +30 -0
- package/dist/polyfill-connectors/src/safe-text-preview.js +156 -0
- package/dist/polyfill-connectors/src/schema-registry.js +17 -0
- package/dist/polyfill-connectors/src/scope-filters.d.ts +38 -0
- package/dist/polyfill-connectors/src/scope-filters.js +80 -0
- package/dist/polyfill-connectors/src/shutdown-hook.js +51 -0
- package/dist/polyfill-connectors/src/streaming-target-registration.js +161 -0
- package/package.json +63 -0
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
import { rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
import { resolveAuth } from "./auth.js";
|
|
5
|
+
import { manualAction } from "./browser-handoff.js";
|
|
6
|
+
import { createCaptureSession } from "./fixture-capture.js";
|
|
7
|
+
import { emitToStdout } from "./safe-emit.js";
|
|
8
|
+
import { resourceSet } from "./scope-filters.js";
|
|
9
|
+
const DEFAULT_RETRYABLE_PATTERN = /ECONN|ETIMEDOUT|timeout/i;
|
|
10
|
+
const TRACE_TIMESTAMP_UNSAFE = /[:.]/g;
|
|
11
|
+
class TerminalError extends Error {
|
|
12
|
+
retryable;
|
|
13
|
+
constructor(message, retryable = false) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "TerminalError";
|
|
16
|
+
this.retryable = retryable;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function isOutsideTimeRange(timeRange, dateValue) {
|
|
20
|
+
if (typeof dateValue !== "string" || !dateValue) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (timeRange.since && dateValue < timeRange.since.slice(0, 10)) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
if (timeRange.until && dateValue >= timeRange.until.slice(0, 10)) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
function makeShapeCheckSkip(stream, data, issues) {
|
|
32
|
+
const message = `${String(data.id)}: ${issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`;
|
|
33
|
+
return {
|
|
34
|
+
type: "SKIP_RESULT",
|
|
35
|
+
stream,
|
|
36
|
+
reason: "shape_check_failed",
|
|
37
|
+
message,
|
|
38
|
+
diagnostics: { id: data.id, issues, record: data },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export const nowIso = () => new Date().toISOString();
|
|
42
|
+
export const politeDelay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
43
|
+
export function runConnector(config) {
|
|
44
|
+
if (!config.name) {
|
|
45
|
+
throw new Error("runConnector: config.name required");
|
|
46
|
+
}
|
|
47
|
+
if (typeof config.collect !== "function") {
|
|
48
|
+
throw new Error("runConnector: config.collect required");
|
|
49
|
+
}
|
|
50
|
+
const { name, validateRecord, collect, browser, retryablePattern = DEFAULT_RETRYABLE_PATTERN, timeRangeField = "date", isTombstone, auth, } = config;
|
|
51
|
+
const ensureSession = browser ? config.ensureSession : undefined;
|
|
52
|
+
const probeSession = browser ? config.probeSession : undefined;
|
|
53
|
+
const timeRangeFieldFor = typeof timeRangeField === "function" ? timeRangeField : () => timeRangeField;
|
|
54
|
+
const capture = createCaptureSession(name);
|
|
55
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
56
|
+
const emit = (msg) => {
|
|
57
|
+
if (capture && msg.type === "RECORD") {
|
|
58
|
+
capture.recordRecord(msg);
|
|
59
|
+
}
|
|
60
|
+
return emitToStdout(msg);
|
|
61
|
+
};
|
|
62
|
+
if (capture) {
|
|
63
|
+
const modeLabel = capture.keepOnSuccess
|
|
64
|
+
? "PDPP_CAPTURE_FIXTURES=1 (always retain)"
|
|
65
|
+
: "PDPP_CAPTURE_ON_FAILURE=1 (retain on failure only)";
|
|
66
|
+
process.stderr.write(`[capture] ${modeLabel}; writing to ${capture.baseDir}\n`);
|
|
67
|
+
}
|
|
68
|
+
const flushAndExit = (code) => {
|
|
69
|
+
if (process.stdout.writableLength > 0) {
|
|
70
|
+
process.stdout.once("drain", () => process.exit(code));
|
|
71
|
+
setTimeout(() => process.exit(code), 3000).unref();
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
process.exit(code);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
let observedCounters = null;
|
|
78
|
+
const emitFailed = (message, retryable = false, records_emitted = observedCounters?.totalEmitted ?? 0) => {
|
|
79
|
+
emit({
|
|
80
|
+
type: "DONE",
|
|
81
|
+
status: "failed",
|
|
82
|
+
records_emitted,
|
|
83
|
+
error: { message, retryable },
|
|
84
|
+
}).catch(() => undefined);
|
|
85
|
+
flushAndExit(1);
|
|
86
|
+
};
|
|
87
|
+
let interactionCounter = 0;
|
|
88
|
+
const nextInteractionId = () => `int_${Date.now()}_${++interactionCounter}`;
|
|
89
|
+
let assistanceCounter = 0;
|
|
90
|
+
const nextAssistanceId = () => `asst_${Date.now()}_${++assistanceCounter}`;
|
|
91
|
+
const sendInteraction = (req) => {
|
|
92
|
+
const request_id = req.request_id ?? nextInteractionId();
|
|
93
|
+
const wrapped = {
|
|
94
|
+
type: "INTERACTION",
|
|
95
|
+
request_id,
|
|
96
|
+
kind: req.kind,
|
|
97
|
+
message: req.message,
|
|
98
|
+
...(req.schema === undefined ? {} : { schema: req.schema }),
|
|
99
|
+
...(req.timeout_seconds === undefined ? {} : { timeout_seconds: req.timeout_seconds }),
|
|
100
|
+
};
|
|
101
|
+
emit(wrapped).catch(() => undefined);
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
const onLine = (line) => {
|
|
104
|
+
try {
|
|
105
|
+
const parsed = JSON.parse(line);
|
|
106
|
+
if (parsed.type === "INTERACTION_RESPONSE" && parsed.request_id === request_id) {
|
|
107
|
+
rl.off("line", onLine);
|
|
108
|
+
resolve(parsed);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
rl.on("line", onLine);
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
const readStart = () => new Promise((resolve, reject) => {
|
|
119
|
+
rl.once("line", (line) => {
|
|
120
|
+
try {
|
|
121
|
+
resolve(JSON.parse(line));
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
const progress = (message, extra = {}) => emit({ type: "PROGRESS", message, ...extra });
|
|
129
|
+
const assist = async (req) => {
|
|
130
|
+
const assistance_request_id = req.assistance_request_id ?? nextAssistanceId();
|
|
131
|
+
await emit({ type: "ASSISTANCE", ...req, assistance_request_id });
|
|
132
|
+
return assistance_request_id;
|
|
133
|
+
};
|
|
134
|
+
const completeAssistance = (assistanceRequestId, status, extra = {}) => emit({ type: "ASSISTANCE_STATUS", assistance_request_id: assistanceRequestId, status, ...extra });
|
|
135
|
+
run().catch((err) => {
|
|
136
|
+
if (err instanceof TerminalError) {
|
|
137
|
+
emitFailed(err.message, err.retryable);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
141
|
+
emitFailed(message, retryablePattern.test(message));
|
|
142
|
+
});
|
|
143
|
+
async function run() {
|
|
144
|
+
const startMsg = await parseStart(readStart);
|
|
145
|
+
const requested = buildRequested(startMsg);
|
|
146
|
+
const credentials = await resolveCredentials(auth, {
|
|
147
|
+
sendInteraction,
|
|
148
|
+
connectorName: name,
|
|
149
|
+
});
|
|
150
|
+
const emitRecord = makeEmitRecord({
|
|
151
|
+
requested,
|
|
152
|
+
emit,
|
|
153
|
+
emittedAt: nowIso(),
|
|
154
|
+
validateRecord,
|
|
155
|
+
isTombstone,
|
|
156
|
+
timeRangeFieldFor,
|
|
157
|
+
});
|
|
158
|
+
observedCounters = emitRecord.counters;
|
|
159
|
+
const emittedAt = nowIso();
|
|
160
|
+
const baseCtx = {
|
|
161
|
+
scope: startMsg.scope,
|
|
162
|
+
state: startMsg.state ?? {},
|
|
163
|
+
requested,
|
|
164
|
+
credentials,
|
|
165
|
+
emit,
|
|
166
|
+
emitRecord: emitRecord.emit,
|
|
167
|
+
assist,
|
|
168
|
+
completeAssistance,
|
|
169
|
+
progress,
|
|
170
|
+
capture,
|
|
171
|
+
sendInteraction,
|
|
172
|
+
emittedAt,
|
|
173
|
+
detailGaps: startMsg.detail_gaps ?? [],
|
|
174
|
+
};
|
|
175
|
+
if (browser) {
|
|
176
|
+
await runInBrowser({
|
|
177
|
+
browser,
|
|
178
|
+
name,
|
|
179
|
+
sendInteraction,
|
|
180
|
+
assist,
|
|
181
|
+
completeAssistance,
|
|
182
|
+
progress,
|
|
183
|
+
ensureSession,
|
|
184
|
+
probeSession,
|
|
185
|
+
collect,
|
|
186
|
+
baseCtx,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
await collect(baseCtx);
|
|
191
|
+
}
|
|
192
|
+
await finalizeRun(emitRecord.counters, progress, emit);
|
|
193
|
+
flushAndExit(0);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async function parseStart(readStart) {
|
|
197
|
+
const startMsg = await readStart();
|
|
198
|
+
if (startMsg.type !== "START") {
|
|
199
|
+
throw new TerminalError("Expected START message", false);
|
|
200
|
+
}
|
|
201
|
+
return startMsg;
|
|
202
|
+
}
|
|
203
|
+
function buildRequested(startMsg) {
|
|
204
|
+
const requested = new Map((startMsg.scope.streams ?? []).map((s) => [s.name, s]));
|
|
205
|
+
if (requested.size === 0) {
|
|
206
|
+
throw new TerminalError("START.scope.streams is required", false);
|
|
207
|
+
}
|
|
208
|
+
return requested;
|
|
209
|
+
}
|
|
210
|
+
async function resolveCredentials(auth, ctx) {
|
|
211
|
+
if (!auth) {
|
|
212
|
+
return {};
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
return await resolveAuth(auth, ctx);
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
219
|
+
throw new TerminalError(message, false);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function makeEmitRecord(deps) {
|
|
223
|
+
const { requested, emit, emittedAt, validateRecord, isTombstone, timeRangeFieldFor } = deps;
|
|
224
|
+
const counters = { totalEmitted: 0, totalSkipped: 0 };
|
|
225
|
+
const resFilters = new Map();
|
|
226
|
+
for (const [streamName, scope] of requested) {
|
|
227
|
+
resFilters.set(streamName, resourceSet(scope));
|
|
228
|
+
}
|
|
229
|
+
const emitRecord = (stream, data) => {
|
|
230
|
+
if (data.id == null) {
|
|
231
|
+
return Promise.resolve();
|
|
232
|
+
}
|
|
233
|
+
const rs = resFilters.get(stream);
|
|
234
|
+
if (rs && !rs.has(String(data.id))) {
|
|
235
|
+
return Promise.resolve();
|
|
236
|
+
}
|
|
237
|
+
if (isTombstone?.(stream, data)) {
|
|
238
|
+
counters.totalEmitted++;
|
|
239
|
+
return emit({
|
|
240
|
+
type: "RECORD",
|
|
241
|
+
stream,
|
|
242
|
+
key: data.id,
|
|
243
|
+
data: { id: data.id },
|
|
244
|
+
emitted_at: emittedAt,
|
|
245
|
+
op: "delete",
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const streamScope = requested.get(stream);
|
|
249
|
+
const field = timeRangeFieldFor(stream);
|
|
250
|
+
if (streamScope?.time_range && isOutsideTimeRange(streamScope.time_range, data[field])) {
|
|
251
|
+
return Promise.resolve();
|
|
252
|
+
}
|
|
253
|
+
if (validateRecord) {
|
|
254
|
+
const result = validateRecord(stream, data);
|
|
255
|
+
if (!result.ok) {
|
|
256
|
+
counters.totalSkipped++;
|
|
257
|
+
return emit(makeShapeCheckSkip(stream, data, result.issues));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
counters.totalEmitted++;
|
|
261
|
+
return emit({
|
|
262
|
+
type: "RECORD",
|
|
263
|
+
stream,
|
|
264
|
+
key: data.id,
|
|
265
|
+
data,
|
|
266
|
+
emitted_at: emittedAt,
|
|
267
|
+
});
|
|
268
|
+
};
|
|
269
|
+
return { emit: emitRecord, counters };
|
|
270
|
+
}
|
|
271
|
+
async function runInBrowser(args) {
|
|
272
|
+
const { browser, name, sendInteraction, assist, completeAssistance, progress, ensureSession, probeSession, collect, baseCtx, } = args;
|
|
273
|
+
const { context: ctx, release } = await acquireBrowser(browser, name);
|
|
274
|
+
const visibility = resolveBrowserRuntimeVisibility(browser, name);
|
|
275
|
+
const browserSendInteraction = makeBrowserInteractionKeepalive({
|
|
276
|
+
context: ctx,
|
|
277
|
+
diagnostics: process.env.PDPP_BROWSER_SURFACE_DIAGNOSTICS === "1",
|
|
278
|
+
progress,
|
|
279
|
+
sendInteraction: (req) => sendInteraction(decorateBrowserManualAction(req, visibility)),
|
|
280
|
+
});
|
|
281
|
+
const { withShutdownRelease } = await import("./shutdown-hook.js");
|
|
282
|
+
const tracer = makeTracer(ctx, name, baseCtx.capture);
|
|
283
|
+
let traceFinalized = false;
|
|
284
|
+
const finalizeDiagnostics = async () => {
|
|
285
|
+
if (traceFinalized) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
traceFinalized = true;
|
|
289
|
+
baseCtx.capture?.setTraceCheckpointHook?.(null);
|
|
290
|
+
await tracer.stop();
|
|
291
|
+
};
|
|
292
|
+
const disposeShutdownHook = withShutdownRelease(release, { finalize: finalizeDiagnostics });
|
|
293
|
+
await tracer.start();
|
|
294
|
+
baseCtx.capture?.setTraceCheckpointHook?.((label) => tracer.checkpoint(label));
|
|
295
|
+
let page = null;
|
|
296
|
+
try {
|
|
297
|
+
page = await ctx.newPage();
|
|
298
|
+
await captureBrowserPage(baseCtx.capture, page, "runtime-new-page");
|
|
299
|
+
await closeBrowserContextPagesExcept(ctx, page);
|
|
300
|
+
await establishSession({ ensureSession, probeSession }, {
|
|
301
|
+
assist,
|
|
302
|
+
capture: baseCtx.capture,
|
|
303
|
+
completeAssistance,
|
|
304
|
+
context: ctx,
|
|
305
|
+
page,
|
|
306
|
+
name,
|
|
307
|
+
progress,
|
|
308
|
+
sendInteraction: browserSendInteraction,
|
|
309
|
+
});
|
|
310
|
+
await captureBrowserPage(baseCtx.capture, page, "runtime-session-established");
|
|
311
|
+
await captureBrowserPage(baseCtx.capture, page, "runtime-collect-start");
|
|
312
|
+
await collect({ ...baseCtx, context: ctx, page, sendInteraction: browserSendInteraction });
|
|
313
|
+
await captureBrowserPage(baseCtx.capture, page, "runtime-collect-complete");
|
|
314
|
+
tracer.markSucceeded();
|
|
315
|
+
baseCtx.capture?.markSucceeded?.();
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
if (page) {
|
|
319
|
+
await captureBrowserPage(baseCtx.capture, page, "runtime-error");
|
|
320
|
+
}
|
|
321
|
+
throw err;
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
await finalizeDiagnostics();
|
|
325
|
+
await closeBrowserPage(page);
|
|
326
|
+
await release().catch(() => undefined);
|
|
327
|
+
disposeShutdownHook();
|
|
328
|
+
baseCtx.capture?.finalize?.();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
export async function captureBrowserPage(capture, page, label) {
|
|
332
|
+
if (!capture) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (page.isClosed()) {
|
|
336
|
+
process.stderr.write(`[capture] page already closed at ${label}; skipping dom snapshot\n`);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
await capture.captureDom(page, label);
|
|
340
|
+
}
|
|
341
|
+
export async function closeBrowserContextPagesExcept(context, keepPage) {
|
|
342
|
+
let pages;
|
|
343
|
+
try {
|
|
344
|
+
pages = context.pages();
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
let closed = 0;
|
|
350
|
+
for (const page of pages) {
|
|
351
|
+
if (page === keepPage || page.isClosed()) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
await page.close();
|
|
356
|
+
closed++;
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return closed;
|
|
362
|
+
}
|
|
363
|
+
export async function closeBrowserPage(page) {
|
|
364
|
+
if (!page || page.isClosed()) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
await page.close();
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const BROWSER_INTERACTION_KEEPALIVE_INTERVAL_MS = 15_000;
|
|
376
|
+
export function makeBrowserInteractionKeepalive(args) {
|
|
377
|
+
const { context, diagnostics = false, intervalMs = BROWSER_INTERACTION_KEEPALIVE_INTERVAL_MS, progress, sendInteraction, } = args;
|
|
378
|
+
return async (req) => {
|
|
379
|
+
await emitBrowserSurfaceDiagnostic({ context, diagnostics, phase: "interaction_start", progress, req });
|
|
380
|
+
const keepalive = startBrowserConnectionKeepalive(context, intervalMs);
|
|
381
|
+
try {
|
|
382
|
+
const response = await sendInteraction(req);
|
|
383
|
+
await emitBrowserSurfaceDiagnostic({
|
|
384
|
+
context,
|
|
385
|
+
diagnostics,
|
|
386
|
+
keepalive: keepalive.stop(),
|
|
387
|
+
phase: "interaction_response",
|
|
388
|
+
progress,
|
|
389
|
+
req,
|
|
390
|
+
responseStatus: response.status,
|
|
391
|
+
});
|
|
392
|
+
return response;
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
await emitBrowserSurfaceDiagnostic({
|
|
396
|
+
context,
|
|
397
|
+
diagnostics,
|
|
398
|
+
error: err,
|
|
399
|
+
keepalive: keepalive.stop(),
|
|
400
|
+
phase: "interaction_error",
|
|
401
|
+
progress,
|
|
402
|
+
req,
|
|
403
|
+
});
|
|
404
|
+
throw err;
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
async function emitBrowserSurfaceDiagnostic(args) {
|
|
409
|
+
const { context, diagnostics, error, keepalive, phase, progress, req, responseStatus } = args;
|
|
410
|
+
if (!(diagnostics && progress)) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
let errorMessage = null;
|
|
414
|
+
if (error instanceof Error) {
|
|
415
|
+
errorMessage = error.message;
|
|
416
|
+
}
|
|
417
|
+
else if (error != null) {
|
|
418
|
+
errorMessage = String(error);
|
|
419
|
+
}
|
|
420
|
+
const payload = {
|
|
421
|
+
phase,
|
|
422
|
+
interaction_kind: req.kind,
|
|
423
|
+
request_id: req.request_id ?? null,
|
|
424
|
+
response_status: responseStatus ?? null,
|
|
425
|
+
surface: describeBrowserSurface(context),
|
|
426
|
+
keepalive: keepalive ?? null,
|
|
427
|
+
error: errorMessage,
|
|
428
|
+
};
|
|
429
|
+
try {
|
|
430
|
+
await progress(`browser_surface.diagnostic ${JSON.stringify(payload)}`);
|
|
431
|
+
}
|
|
432
|
+
catch (progressError) {
|
|
433
|
+
const message = progressError instanceof Error ? progressError.message : String(progressError);
|
|
434
|
+
process.stderr.write(`[browser-surface-diagnostics] progress emit failed: ${message}\n`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function describeBrowserSurface(context) {
|
|
438
|
+
const browser = context.browser();
|
|
439
|
+
let pages = [];
|
|
440
|
+
try {
|
|
441
|
+
pages = typeof context.pages === "function" ? context.pages() : [];
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
pages = [];
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
browser_connected: Boolean(browser?.isConnected()),
|
|
448
|
+
page_count: typeof context.pages === "function" ? pages.length : null,
|
|
449
|
+
pages: pages.slice(0, 5).map((page) => ({
|
|
450
|
+
closed: page.isClosed(),
|
|
451
|
+
url: sanitizeDiagnosticUrl(page),
|
|
452
|
+
})),
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function sanitizeDiagnosticUrl(page) {
|
|
456
|
+
if (page.isClosed()) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
const rawUrl = page.url();
|
|
461
|
+
if (!rawUrl || rawUrl === "about:blank") {
|
|
462
|
+
return rawUrl || null;
|
|
463
|
+
}
|
|
464
|
+
const url = new URL(rawUrl);
|
|
465
|
+
return `${url.origin}${url.pathname}`;
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
return "unparseable";
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function normalizeDiagnosticError(error) {
|
|
472
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
473
|
+
return raw.slice(0, 300);
|
|
474
|
+
}
|
|
475
|
+
function summarizeInactiveKeepalive(browser, startedAt) {
|
|
476
|
+
return {
|
|
477
|
+
browserConnectedAtStart: Boolean(browser?.isConnected()),
|
|
478
|
+
browserConnectedAtStop: Boolean(browser?.isConnected()),
|
|
479
|
+
elapsedMs: Date.now() - startedAt,
|
|
480
|
+
disconnectEventCount: 0,
|
|
481
|
+
pingAttempts: 0,
|
|
482
|
+
pingFailures: 0,
|
|
483
|
+
pingInFlight: false,
|
|
484
|
+
pingSuccesses: 0,
|
|
485
|
+
skippedDisconnected: 0,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function startBrowserConnectionKeepalive(context, intervalMs) {
|
|
489
|
+
const startedAt = Date.now();
|
|
490
|
+
const browser = context.browser();
|
|
491
|
+
if (intervalMs <= 0 || !browser?.isConnected()) {
|
|
492
|
+
return { stop: () => summarizeInactiveKeepalive(browser, startedAt) };
|
|
493
|
+
}
|
|
494
|
+
let sessionPromise = null;
|
|
495
|
+
let pingInFlight = false;
|
|
496
|
+
let pingAttempts = 0;
|
|
497
|
+
let pingFailures = 0;
|
|
498
|
+
let pingSuccesses = 0;
|
|
499
|
+
let skippedDisconnected = 0;
|
|
500
|
+
let stopped = false;
|
|
501
|
+
let lastError;
|
|
502
|
+
let lastSuccessfulPingElapsedMs;
|
|
503
|
+
let firstObservedDisconnectedElapsedMs;
|
|
504
|
+
let disconnectEventElapsedMs;
|
|
505
|
+
let disconnectEventCount = 0;
|
|
506
|
+
const browserConnectedAtStart = browser.isConnected();
|
|
507
|
+
const removeDisconnectedListener = attachBrowserDisconnectedDiagnostic(browser, () => {
|
|
508
|
+
disconnectEventCount++;
|
|
509
|
+
disconnectEventElapsedMs ??= Date.now() - startedAt;
|
|
510
|
+
process.stderr.write(`[browser-keepalive] browser disconnected during interaction after ${disconnectEventElapsedMs}ms\n`);
|
|
511
|
+
});
|
|
512
|
+
const sessionFor = (connectedBrowser) => {
|
|
513
|
+
sessionPromise ??= connectedBrowser.newBrowserCDPSession();
|
|
514
|
+
return sessionPromise;
|
|
515
|
+
};
|
|
516
|
+
const ping = async () => {
|
|
517
|
+
if (stopped || pingInFlight) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (!browser.isConnected()) {
|
|
521
|
+
firstObservedDisconnectedElapsedMs ??= Date.now() - startedAt;
|
|
522
|
+
skippedDisconnected++;
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
pingInFlight = true;
|
|
526
|
+
pingAttempts++;
|
|
527
|
+
try {
|
|
528
|
+
const session = await sessionFor(browser);
|
|
529
|
+
await session.send("Browser.getVersion");
|
|
530
|
+
pingSuccesses++;
|
|
531
|
+
lastSuccessfulPingElapsedMs = Date.now() - startedAt;
|
|
532
|
+
}
|
|
533
|
+
catch (err) {
|
|
534
|
+
sessionPromise = null;
|
|
535
|
+
pingFailures++;
|
|
536
|
+
lastError = normalizeDiagnosticError(err);
|
|
537
|
+
process.stderr.write(`[browser-keepalive] Browser.getVersion failed: ${lastError}\n`);
|
|
538
|
+
}
|
|
539
|
+
finally {
|
|
540
|
+
pingInFlight = false;
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
const timer = setInterval(ping, intervalMs);
|
|
544
|
+
timer.unref?.();
|
|
545
|
+
ping().catch(() => undefined);
|
|
546
|
+
return {
|
|
547
|
+
stop: () => {
|
|
548
|
+
stopped = true;
|
|
549
|
+
clearInterval(timer);
|
|
550
|
+
removeDisconnectedListener();
|
|
551
|
+
sessionPromise?.then((session) => session.detach()).catch(() => undefined);
|
|
552
|
+
return {
|
|
553
|
+
browserConnectedAtStart,
|
|
554
|
+
browserConnectedAtStop: browser.isConnected(),
|
|
555
|
+
disconnectEventCount,
|
|
556
|
+
...(disconnectEventElapsedMs === undefined ? {} : { disconnectEventElapsedMs }),
|
|
557
|
+
elapsedMs: Date.now() - startedAt,
|
|
558
|
+
...(firstObservedDisconnectedElapsedMs === undefined ? {} : { firstObservedDisconnectedElapsedMs }),
|
|
559
|
+
...(lastSuccessfulPingElapsedMs === undefined ? {} : { lastSuccessfulPingElapsedMs }),
|
|
560
|
+
pingAttempts,
|
|
561
|
+
pingFailures,
|
|
562
|
+
pingInFlight,
|
|
563
|
+
pingSuccesses,
|
|
564
|
+
skippedDisconnected,
|
|
565
|
+
...(lastError ? { lastError } : {}),
|
|
566
|
+
};
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function attachBrowserDisconnectedDiagnostic(browser, onDisconnected) {
|
|
571
|
+
const eventTarget = browser;
|
|
572
|
+
if (typeof eventTarget.on !== "function") {
|
|
573
|
+
return () => undefined;
|
|
574
|
+
}
|
|
575
|
+
eventTarget.on("disconnected", onDisconnected);
|
|
576
|
+
return () => {
|
|
577
|
+
if (typeof eventTarget.off === "function") {
|
|
578
|
+
eventTarget.off("disconnected", onDisconnected);
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
async function finalizeRun(counters, progress, emit) {
|
|
583
|
+
if (counters.totalSkipped > 0) {
|
|
584
|
+
await progress(`shape-check skipped ${String(counters.totalSkipped)} record(s); see SKIP_RESULT events above`);
|
|
585
|
+
}
|
|
586
|
+
await emit({
|
|
587
|
+
type: "DONE",
|
|
588
|
+
status: "succeeded",
|
|
589
|
+
records_emitted: counters.totalEmitted,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
const MANUAL_ACTION_RECOVERY_RE = /\bheadless\b|local collector|rerun .*headed|PDPP_[A-Z0-9_]+_HEADLESS/iu;
|
|
593
|
+
export function resolveBrowserRuntimeVisibility(browser, name, env = process.env) {
|
|
594
|
+
const profileName = browser.profileName ?? name;
|
|
595
|
+
const envKey = `PDPP_${profileName.toUpperCase()}_HEADLESS`;
|
|
596
|
+
return {
|
|
597
|
+
envKey,
|
|
598
|
+
headless: browser.headless ?? env[envKey] !== "0",
|
|
599
|
+
profileName,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
export function resolveBrowserLaunchSource(visibility, env = process.env) {
|
|
603
|
+
const managedRequired = env.PDPP_BROWSER_SURFACE_REQUIRED?.trim().toLowerCase() === "neko";
|
|
604
|
+
const managedRemoteCdpUrl = env.PDPP_BROWSER_SURFACE_REMOTE_CDP_URL?.trim();
|
|
605
|
+
if (managedRequired) {
|
|
606
|
+
if (!managedRemoteCdpUrl) {
|
|
607
|
+
throw new TerminalError("browser surface required: PDPP_BROWSER_SURFACE_REQUIRED=neko but PDPP_BROWSER_SURFACE_REMOTE_CDP_URL is missing", false);
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
kind: "managed_neko",
|
|
611
|
+
remoteCdpUrl: managedRemoteCdpUrl,
|
|
612
|
+
...(env.PDPP_BROWSER_SURFACE_LEASE_ID?.trim() ? { leaseId: env.PDPP_BROWSER_SURFACE_LEASE_ID.trim() } : {}),
|
|
613
|
+
...(env.PDPP_BROWSER_SURFACE_PROFILE_KEY?.trim()
|
|
614
|
+
? { profileKey: env.PDPP_BROWSER_SURFACE_PROFILE_KEY.trim() }
|
|
615
|
+
: {}),
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
const legacyRemoteCdpEnvKey = `PDPP_${visibility.profileName.toUpperCase()}_REMOTE_CDP_URL`;
|
|
619
|
+
const legacyRemoteCdpUrl = env[legacyRemoteCdpEnvKey]?.trim();
|
|
620
|
+
if (legacyRemoteCdpUrl) {
|
|
621
|
+
return {
|
|
622
|
+
envKey: legacyRemoteCdpEnvKey,
|
|
623
|
+
kind: "legacy_remote_cdp",
|
|
624
|
+
remoteCdpUrl: legacyRemoteCdpUrl,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
return { kind: "isolated_local" };
|
|
628
|
+
}
|
|
629
|
+
export function decorateBrowserManualAction(req, visibility) {
|
|
630
|
+
if (req.kind !== "manual_action") {
|
|
631
|
+
return req;
|
|
632
|
+
}
|
|
633
|
+
if (!visibility.headless) {
|
|
634
|
+
return req;
|
|
635
|
+
}
|
|
636
|
+
if (MANUAL_ACTION_RECOVERY_RE.test(req.message)) {
|
|
637
|
+
return req;
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
...req,
|
|
641
|
+
message: `${req.message}\n\n` +
|
|
642
|
+
"Open the streaming companion to drive the connector's browser from your phone or laptop. " +
|
|
643
|
+
`Or rerun with ${visibility.envKey}=0 on a host desktop to use a visible local browser instead.`,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
async function acquireBrowser(browser, name) {
|
|
647
|
+
const { acquireBrowserForConnector, HeadedBrowserUnavailableError } = await import("./browser-launch.js");
|
|
648
|
+
const visibility = resolveBrowserRuntimeVisibility(browser, name);
|
|
649
|
+
const { headless, profileName } = visibility;
|
|
650
|
+
const streamingEnabled = Boolean(process.env.PDPP_RUN_ID?.trim()) &&
|
|
651
|
+
Boolean(process.env.PDPP_REFERENCE_BASE_URL?.trim()) &&
|
|
652
|
+
Boolean(process.env.PDPP_STREAMING_REGISTRATION_TOKEN?.trim() || process.env.PDPP_LOCAL_DEVICE_TOKEN?.trim());
|
|
653
|
+
const launchSource = resolveBrowserLaunchSource(visibility);
|
|
654
|
+
const remoteCdpUrl = launchSource.kind === "managed_neko" || launchSource.kind === "legacy_remote_cdp"
|
|
655
|
+
? launchSource.remoteCdpUrl
|
|
656
|
+
: undefined;
|
|
657
|
+
try {
|
|
658
|
+
return await acquireBrowserForConnector({
|
|
659
|
+
profileName,
|
|
660
|
+
headless,
|
|
661
|
+
...(streamingEnabled ? { streamingEnabled: true } : {}),
|
|
662
|
+
...(remoteCdpUrl ? { remoteCdpUrl } : {}),
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
catch (err) {
|
|
666
|
+
if (err instanceof HeadedBrowserUnavailableError) {
|
|
667
|
+
throw new TerminalError(`[${err.code}] ${err.message}`, false);
|
|
668
|
+
}
|
|
669
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
670
|
+
throw new TerminalError(`could not open browser profile: ${message}`, false);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
async function establishSession(hooks, args) {
|
|
674
|
+
const { ensureSession, probeSession } = hooks;
|
|
675
|
+
const { assist, capture, completeAssistance, context, page, name, sendInteraction, progress } = args;
|
|
676
|
+
if (typeof ensureSession === "function") {
|
|
677
|
+
try {
|
|
678
|
+
await ensureSession({ assist, capture, completeAssistance, context, page, sendInteraction, progress });
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
catch (err) {
|
|
682
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
683
|
+
throw new TerminalError(`${name}_session_failed: ${message}`, false);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (typeof probeSession !== "function") {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (await probeSession({ context, page })) {
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
await manualAction({
|
|
693
|
+
page,
|
|
694
|
+
reason: "login",
|
|
695
|
+
message: `${name} session expired. Open the browser and re-authenticate, then continue.`,
|
|
696
|
+
timeoutSeconds: 1800,
|
|
697
|
+
}, sendInteraction);
|
|
698
|
+
if (await probeSession({ context, page })) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
throw new TerminalError(`${name}_session_required`, false);
|
|
702
|
+
}
|
|
703
|
+
export function isContextDisconnected(context) {
|
|
704
|
+
try {
|
|
705
|
+
const browser = context.browser?.();
|
|
706
|
+
if (!browser) {
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
if (typeof browser.isConnected === "function") {
|
|
710
|
+
return browser.isConnected() === false;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
catch {
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
export function makeTracer(context, name, capture) {
|
|
719
|
+
const enabled = process.env.PDPP_TRACE === "1" || capture !== null;
|
|
720
|
+
const traceName = `${name}-${new Date().toISOString().replace(TRACE_TIMESTAMP_UNSAFE, "-")}`;
|
|
721
|
+
const tracePath = capture ? join(capture.baseDir, "traces", `${traceName}.zip`) : `/tmp/${traceName}.zip`;
|
|
722
|
+
const traceBaseDir = capture ? join(capture.baseDir, "traces") : null;
|
|
723
|
+
const tracing = context.tracing;
|
|
724
|
+
let started = false;
|
|
725
|
+
let chunkStarted = false;
|
|
726
|
+
let chunkSeq = 0;
|
|
727
|
+
let succeeded = false;
|
|
728
|
+
const writtenTraceFiles = [];
|
|
729
|
+
const safeChunkLabel = (label) => String(label)
|
|
730
|
+
.replace(/[^A-Za-z0-9_.-]/g, "_")
|
|
731
|
+
.slice(0, 80);
|
|
732
|
+
const writeTraceDiagnostic = (phase, err) => {
|
|
733
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
734
|
+
process.stderr.write(`[trace] ${phase} failed: ${message}\n`);
|
|
735
|
+
if (!traceBaseDir) {
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
try {
|
|
739
|
+
writeFileSync(join(traceBaseDir, `${traceName}-${String(chunkSeq).padStart(3, "0")}-${safeChunkLabel(phase)}.error.json`), JSON.stringify({
|
|
740
|
+
captured_at: new Date().toISOString(),
|
|
741
|
+
error: message,
|
|
742
|
+
phase,
|
|
743
|
+
}, null, 2));
|
|
744
|
+
}
|
|
745
|
+
catch {
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
const startChunk = async (label) => {
|
|
749
|
+
if (!traceBaseDir || typeof tracing.startChunk !== "function") {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
try {
|
|
753
|
+
await tracing.startChunk({ title: `${traceName}:${label}` });
|
|
754
|
+
chunkStarted = true;
|
|
755
|
+
}
|
|
756
|
+
catch (err) {
|
|
757
|
+
chunkStarted = false;
|
|
758
|
+
writeTraceDiagnostic("start-chunk", err);
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
const stopChunk = async (label) => {
|
|
762
|
+
if (!(traceBaseDir && chunkStarted) || typeof tracing.stopChunk !== "function") {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
chunkSeq += 1;
|
|
766
|
+
const path = join(traceBaseDir, `${traceName}-${String(chunkSeq).padStart(3, "0")}-${safeChunkLabel(label)}.zip`);
|
|
767
|
+
try {
|
|
768
|
+
await tracing.stopChunk({ path });
|
|
769
|
+
writtenTraceFiles.push(path);
|
|
770
|
+
}
|
|
771
|
+
catch (err) {
|
|
772
|
+
writeTraceDiagnostic(`stop-chunk-${safeChunkLabel(label)}`, err);
|
|
773
|
+
}
|
|
774
|
+
finally {
|
|
775
|
+
chunkStarted = false;
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
const deleteWrittenTraces = () => {
|
|
779
|
+
for (const path of writtenTraceFiles) {
|
|
780
|
+
try {
|
|
781
|
+
rmSync(path, { force: true });
|
|
782
|
+
}
|
|
783
|
+
catch (err) {
|
|
784
|
+
writeTraceDiagnostic("delete-on-success", err);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
writtenTraceFiles.length = 0;
|
|
788
|
+
};
|
|
789
|
+
return {
|
|
790
|
+
async start() {
|
|
791
|
+
if (!enabled) {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
try {
|
|
795
|
+
await context.tracing.start({
|
|
796
|
+
name: traceName,
|
|
797
|
+
screenshots: true,
|
|
798
|
+
snapshots: true,
|
|
799
|
+
sources: true,
|
|
800
|
+
});
|
|
801
|
+
started = true;
|
|
802
|
+
}
|
|
803
|
+
catch (err) {
|
|
804
|
+
writeTraceDiagnostic("start", err);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
await startChunk("start");
|
|
808
|
+
process.stderr.write(`[trace] tracing enabled; ${traceBaseDir ? `writing chunks under ${traceBaseDir}` : `will write ${tracePath} on exit`}\n`);
|
|
809
|
+
},
|
|
810
|
+
async checkpoint(label) {
|
|
811
|
+
if (!(enabled && started && traceBaseDir) || typeof tracing.startChunk !== "function") {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
await stopChunk(label);
|
|
815
|
+
await startChunk(label);
|
|
816
|
+
},
|
|
817
|
+
markSucceeded() {
|
|
818
|
+
succeeded = true;
|
|
819
|
+
},
|
|
820
|
+
async stop() {
|
|
821
|
+
if (!(enabled && started)) {
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
started = false;
|
|
825
|
+
if (isContextDisconnected(context)) {
|
|
826
|
+
finalizeDisconnected();
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
if (traceBaseDir && typeof tracing.stopChunk === "function") {
|
|
831
|
+
await stopChunkedTrace();
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
await stopSingleTrace();
|
|
835
|
+
}
|
|
836
|
+
catch (err) {
|
|
837
|
+
writeTraceDiagnostic("stop", err);
|
|
838
|
+
}
|
|
839
|
+
},
|
|
840
|
+
};
|
|
841
|
+
function finalizeDisconnected() {
|
|
842
|
+
writeTraceDiagnostic("stop-disconnected", new Error("browser disconnected before trace stop"));
|
|
843
|
+
if (!traceBaseDir) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (succeeded) {
|
|
847
|
+
deleteWrittenTraces();
|
|
848
|
+
process.stderr.write(`[trace] run succeeded but browser disconnected; trace chunks deleted from ${traceBaseDir}\n`);
|
|
849
|
+
}
|
|
850
|
+
else {
|
|
851
|
+
process.stderr.write(`[trace] browser disconnected before stop; chunks retained under ${traceBaseDir}\n`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
async function stopChunkedTrace() {
|
|
855
|
+
await stopChunk("final");
|
|
856
|
+
await context.tracing.stop();
|
|
857
|
+
if (succeeded) {
|
|
858
|
+
deleteWrittenTraces();
|
|
859
|
+
process.stderr.write(`[trace] run succeeded; trace chunks deleted from ${traceBaseDir}\n`);
|
|
860
|
+
}
|
|
861
|
+
else {
|
|
862
|
+
process.stderr.write(`[trace] run failed; trace chunks retained under ${traceBaseDir}\n`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
async function stopSingleTrace() {
|
|
866
|
+
await context.tracing.stop({ path: tracePath });
|
|
867
|
+
if (!succeeded) {
|
|
868
|
+
process.stderr.write(`[trace] run failed; trace retained at ${tracePath}\n`);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
try {
|
|
872
|
+
rmSync(tracePath, { force: true });
|
|
873
|
+
process.stderr.write(`[trace] run succeeded; trace deleted (${tracePath})\n`);
|
|
874
|
+
}
|
|
875
|
+
catch (err) {
|
|
876
|
+
writeTraceDiagnostic("delete-on-success", err);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|