cdp-toolkit 1.9.0 → 1.9.4
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 +507 -0
- package/CONTRACT.md +62 -5
- package/README.md +74 -10
- package/dist/cli.js +2940 -138
- package/dist/index.js +994 -90
- package/dist/mcp.js +1221 -137
- package/package.json +2 -1
- package/src/backend.ts +156 -21
- package/src/bidi/client.ts +48 -10
- package/src/bidi/driver.ts +228 -33
- package/src/bidi/launch.ts +23 -13
- package/src/bidi-tools.ts +21 -0
- package/src/cdp/driver.ts +594 -9
- package/src/cli.ts +111 -12
- package/src/driver.ts +254 -3
- package/src/manifest.ts +42 -14
- package/src/mcp.ts +21 -12
- package/src/png.ts +809 -0
- package/src/shared-tools.ts +313 -11
- package/src/tools/console.ts +24 -7
- package/src/tools/network.ts +21 -7
- package/src/tools/recorder.ts +110 -10
- package/src/types.ts +8 -6
- package/src/workers.ts +69 -7
package/dist/index.js
CHANGED
|
@@ -404,6 +404,12 @@ async function resolveLiveLabel(backend, label, liveIds) {
|
|
|
404
404
|
|
|
405
405
|
// src/workers.ts
|
|
406
406
|
var WORKER_SELECTOR_PREFIX = "worker:";
|
|
407
|
+
var WORKER_CAPABLE_TOOLS = [
|
|
408
|
+
"evaluate_script",
|
|
409
|
+
"list_network_requests",
|
|
410
|
+
"get_network_request",
|
|
411
|
+
"list_console_messages"
|
|
412
|
+
];
|
|
407
413
|
var WORKER_TARGET_TYPES = new Set(["service_worker", "shared_worker"]);
|
|
408
414
|
function isWorkerSelector(selector) {
|
|
409
415
|
return typeof selector === "string" && selector.startsWith(WORKER_SELECTOR_PREFIX);
|
|
@@ -422,8 +428,18 @@ function resolveWorkerTargets(targets, needle) {
|
|
|
422
428
|
};
|
|
423
429
|
}
|
|
424
430
|
var WORKER_EMPTY_NEEDLE_MESSAGE = "'worker:' needs a substring of the worker's url, e.g. worker:<extension-id> or worker:background.js";
|
|
425
|
-
var WORKER_SELECTOR_UNSUPPORTED_MESSAGE = "target 'worker:<substring>' selects a service/shared worker, which this backend cannot address " + "(WebDriver BiDi has no extension service-worker target; Capability 'worker.targets' is Chrome-only). " + "Use --browser chrome to evaluate in an extension's background worker.";
|
|
426
|
-
var WORKER_SELECTOR_PAGE_ONLY_MESSAGE = "target 'worker:<substring>' selects a service/shared worker, and this tool is page-only " + "(closing, selecting or releasing a worker is meaningless).
|
|
431
|
+
var WORKER_SELECTOR_UNSUPPORTED_MESSAGE = "target 'worker:<substring>' selects a service/shared worker, which this backend cannot address " + "(WebDriver BiDi has no extension service-worker target; Capability 'worker.targets' is Chrome-only). " + "Use --browser chrome to evaluate in, or record console/network from, an extension's background worker.";
|
|
432
|
+
var WORKER_SELECTOR_PAGE_ONLY_MESSAGE = "target 'worker:<substring>' selects a service/shared worker, and this tool is page-only " + "(closing, selecting or releasing a worker is meaningless). The tools that accept a worker: target are " + `${WORKER_CAPABLE_TOOLS.join(", ")}.`;
|
|
433
|
+
function workerWakeMisuseMessage(kind) {
|
|
434
|
+
if (kind === "not-a-worker-selector") {
|
|
435
|
+
return "'wake' only applies to a target of the form 'worker:<substring>'. A page is never asleep, and a bare " + "worker target id cannot be woken either: Chrome destroys the target when an MV3 worker is evicted and " + "mints a NEW id when it restarts, so only the url-substring form can name a worker that is not running.";
|
|
436
|
+
}
|
|
437
|
+
return "'wake' only applies when a capture is being started (reload:true, or includeBody with url). Reading an " + "existing buffer cannot use a woken worker: a restarted MV3 worker gets a NEW target id, and the buffer is " + "keyed by id, so the wake would hand back an empty capture. Re-run with reload:true to record a fresh one.";
|
|
438
|
+
}
|
|
439
|
+
function workerBufferReadMissMessage(needle, liveWorkers) {
|
|
440
|
+
const running = liveWorkers.length ? ` (running workers: ${urlList(liveWorkers)})` : " (no workers are running)";
|
|
441
|
+
return `no running worker whose url contains '${needle}'${running}. ` + "An MV3 extension service worker is idle-evicted after seconds of inactivity and then appears in NO target " + "listing, so this is usually a sleeping worker rather than a wrong substring. This call only READS a buffer " + "recorded earlier, and that buffer is keyed by the worker's target id, which Chrome re-mints on restart — so " + "re-run with reload:true, which starts the worker and records a fresh capture window.";
|
|
442
|
+
}
|
|
427
443
|
function urlList(workers) {
|
|
428
444
|
return workers.map((w) => w.url).join(", ");
|
|
429
445
|
}
|
|
@@ -1103,6 +1119,8 @@ async function clearMocks(args = {}) {
|
|
|
1103
1119
|
}
|
|
1104
1120
|
|
|
1105
1121
|
// src/driver.ts
|
|
1122
|
+
import { mkdir as mkdir3, readFile as readFile3, unlink as unlink3, writeFile as writeFile3 } from "node:fs/promises";
|
|
1123
|
+
import { join as join3 } from "node:path";
|
|
1106
1124
|
var UID_STAMP_ATTR = "data-cdp-uid";
|
|
1107
1125
|
var LEGACY_NUMERIC_UID = /^[0-9]+$/;
|
|
1108
1126
|
var REQUIRED_CAPABILITIES = {
|
|
@@ -1132,6 +1150,41 @@ function interpolatePoints(from, to, steps) {
|
|
|
1132
1150
|
out.push({ x: to.x, y: to.y });
|
|
1133
1151
|
return out;
|
|
1134
1152
|
}
|
|
1153
|
+
function isTiledCapture(delivery) {
|
|
1154
|
+
return delivery.tiled === true;
|
|
1155
|
+
}
|
|
1156
|
+
function viewportRecordFile(scheme, targetId) {
|
|
1157
|
+
return join3(leaseDir(), `viewport-${scheme}-${targetId.replace(/[^A-Za-z0-9._-]/g, "_")}.json`);
|
|
1158
|
+
}
|
|
1159
|
+
async function readViewportRecord(scheme, targetId) {
|
|
1160
|
+
try {
|
|
1161
|
+
const raw = JSON.parse(await readFile3(viewportRecordFile(scheme, targetId), "utf8"));
|
|
1162
|
+
if (typeof raw?.width !== "number" || typeof raw?.height !== "number")
|
|
1163
|
+
return;
|
|
1164
|
+
return {
|
|
1165
|
+
width: raw.width,
|
|
1166
|
+
height: raw.height,
|
|
1167
|
+
...typeof raw.deviceScaleFactor === "number" ? { deviceScaleFactor: raw.deviceScaleFactor } : {},
|
|
1168
|
+
...typeof raw.mobile === "boolean" ? { mobile: raw.mobile } : {}
|
|
1169
|
+
};
|
|
1170
|
+
} catch {
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
async function writeViewportRecord(scheme, targetId, rec) {
|
|
1175
|
+
try {
|
|
1176
|
+
await mkdir3(leaseDir(), { recursive: true });
|
|
1177
|
+
await writeFile3(viewportRecordFile(scheme, targetId), JSON.stringify({ scheme, targetId, ...rec }), "utf8");
|
|
1178
|
+
} catch {}
|
|
1179
|
+
}
|
|
1180
|
+
function renderRestoreFailureNote(size, why) {
|
|
1181
|
+
return `— AND the ${size.width}x${size.height} render-size override could NOT be restored (${why}), so this page is STILL ` + `emulated at ${size.width}x${size.height}: put it back with emulate{width,height} or emulate{clearOverrides:true}.`;
|
|
1182
|
+
}
|
|
1183
|
+
async function clearViewportRecord(scheme, targetId) {
|
|
1184
|
+
try {
|
|
1185
|
+
await unlink3(viewportRecordFile(scheme, targetId));
|
|
1186
|
+
} catch {}
|
|
1187
|
+
}
|
|
1135
1188
|
|
|
1136
1189
|
// src/cdp/driver.ts
|
|
1137
1190
|
function driverError(code, message, data) {
|
|
@@ -1363,14 +1416,73 @@ async function backendNodeIdOf(conn, loc) {
|
|
|
1363
1416
|
}
|
|
1364
1417
|
return searchLocate(conn, "xpath" in loc ? loc.xpath : loc.text);
|
|
1365
1418
|
}
|
|
1366
|
-
async function
|
|
1367
|
-
|
|
1419
|
+
async function layoutMetrics(conn) {
|
|
1420
|
+
return conn.send("Page.getLayoutMetrics");
|
|
1421
|
+
}
|
|
1422
|
+
function contentClip(m) {
|
|
1368
1423
|
const size = m.cssContentSize ?? m.contentSize;
|
|
1369
1424
|
if (!size)
|
|
1370
1425
|
throw driverError("page-error", "Page.getLayoutMetrics returned no content size");
|
|
1371
1426
|
return { x: 0, y: 0, width: Math.ceil(size.width), height: Math.ceil(size.height) };
|
|
1372
1427
|
}
|
|
1373
|
-
|
|
1428
|
+
function devicePixelsPerCssPx(m) {
|
|
1429
|
+
const css = m.cssVisualViewport?.clientWidth ?? m.cssLayoutViewport?.clientWidth;
|
|
1430
|
+
const dev = m.visualViewport?.clientWidth ?? m.layoutViewport?.clientWidth;
|
|
1431
|
+
if (!css || !dev)
|
|
1432
|
+
return 1;
|
|
1433
|
+
const ratio = dev / css;
|
|
1434
|
+
return Number.isFinite(ratio) && ratio > 0 ? ratio : 1;
|
|
1435
|
+
}
|
|
1436
|
+
function scrollOffsetCssPx(m, devicePx) {
|
|
1437
|
+
const css = m.cssVisualViewport ?? m.cssLayoutViewport;
|
|
1438
|
+
if (css)
|
|
1439
|
+
return { x: css.pageX ?? 0, y: css.pageY ?? 0 };
|
|
1440
|
+
const legacy = m.layoutViewport;
|
|
1441
|
+
if (legacy)
|
|
1442
|
+
return { x: (legacy.pageX ?? 0) / devicePx, y: (legacy.pageY ?? 0) / devicePx };
|
|
1443
|
+
return { x: 0, y: 0 };
|
|
1444
|
+
}
|
|
1445
|
+
var MAX_ENCODED_SIDE_PX = 16384;
|
|
1446
|
+
function assertClipEncodable(clip, scale, devicePx, renderSize) {
|
|
1447
|
+
const outWidth = Math.ceil(clip.width * scale * devicePx);
|
|
1448
|
+
const outHeight = Math.ceil(clip.height * scale * devicePx);
|
|
1449
|
+
if (outWidth <= MAX_ENCODED_SIDE_PX && outHeight <= MAX_ENCODED_SIDE_PX)
|
|
1450
|
+
return;
|
|
1451
|
+
const at = renderSize ? ` at render size ${renderSize.width}x${renderSize.height}` : "";
|
|
1452
|
+
const projection = `(clip ${clip.width}x${clip.height} css px${at} x scale ${scale} x device pixel ratio ${devicePx}), ` + `past Chrome's ${MAX_ENCODED_SIDE_PX} px per-side limit; `;
|
|
1453
|
+
if (scale !== 1) {
|
|
1454
|
+
const fits = Math.floor(MAX_ENCODED_SIDE_PX / (Math.max(clip.width, clip.height) * devicePx) * 100) / 100;
|
|
1455
|
+
throw driverError("page-error", `screenshot scale ${scale} would encode ${outWidth}x${outHeight} px ${projection}` + (fits >= 0.01 ? `the largest scale that fits this clip is ${fits}.` : `this clip is already past the limit at scale 1 — capture a smaller region.`));
|
|
1456
|
+
}
|
|
1457
|
+
const maxCssSide = Math.floor(MAX_ENCODED_SIDE_PX / devicePx);
|
|
1458
|
+
throw driverError("page-error", `this capture would encode ${outWidth}x${outHeight} px ${projection}` + `at device pixel ratio ${devicePx} the clip may be at most ${maxCssSide} css px per side` + (renderSize ? ` — a narrow renderWidth reflows the document TALLER, so widen it, shorten the page, or drop fullPage.` : ` — capture a smaller region or drop fullPage.`));
|
|
1459
|
+
}
|
|
1460
|
+
function projectedSide(cssLength, scale, devicePx) {
|
|
1461
|
+
return Math.ceil(cssLength * scale * devicePx);
|
|
1462
|
+
}
|
|
1463
|
+
function planTiledCapture(clip, scale, devicePx, tile) {
|
|
1464
|
+
const untiled = { tiled: false, bandCount: 1, bandHeightCss: clip.height };
|
|
1465
|
+
if (tile === false)
|
|
1466
|
+
return untiled;
|
|
1467
|
+
const fits = projectedSide(clip.width, scale, devicePx) <= MAX_ENCODED_SIDE_PX && projectedSide(clip.height, scale, devicePx) <= MAX_ENCODED_SIDE_PX;
|
|
1468
|
+
if (fits && tile !== true)
|
|
1469
|
+
return untiled;
|
|
1470
|
+
const outWidth = projectedSide(clip.width, scale, devicePx);
|
|
1471
|
+
if (outWidth > MAX_ENCODED_SIDE_PX) {
|
|
1472
|
+
const maxCssWidth = Math.floor(MAX_ENCODED_SIDE_PX / (scale * devicePx));
|
|
1473
|
+
throw driverError("page-error", `this capture is ${outWidth} px WIDE (clip ${clip.width} css px x scale ${scale} x device pixel ratio ${devicePx}), ` + `past Chrome's ${MAX_ENCODED_SIDE_PX} px per-side limit, and width cannot be tiled: bands are stitched ` + `vertically only. Lower scale (at most ${Math.floor(MAX_ENCODED_SIDE_PX / (clip.width * devicePx) * 100) / 100}), ` + `or set renderWidth at most ${maxCssWidth} css px, or clip to a narrower element.`);
|
|
1474
|
+
}
|
|
1475
|
+
let bandHeightCss = Math.floor(MAX_ENCODED_SIDE_PX / (scale * devicePx));
|
|
1476
|
+
while (bandHeightCss > 1 && projectedSide(bandHeightCss, scale, devicePx) > MAX_ENCODED_SIDE_PX)
|
|
1477
|
+
bandHeightCss -= 1;
|
|
1478
|
+
if (bandHeightCss < 1) {
|
|
1479
|
+
throw driverError("page-error", `scale ${scale} at device pixel ratio ${devicePx} projects a single css pixel to ${scale * devicePx} device px, ` + `past the ${MAX_ENCODED_SIDE_PX} px limit — no band height can fit. Lower scale.`);
|
|
1480
|
+
}
|
|
1481
|
+
if (tile === true && clip.height <= bandHeightCss)
|
|
1482
|
+
bandHeightCss = Math.max(1, Math.ceil(clip.height / 2));
|
|
1483
|
+
return { tiled: true, bandCount: Math.max(1, Math.ceil(clip.height / bandHeightCss)), bandHeightCss };
|
|
1484
|
+
}
|
|
1485
|
+
async function elementBox(conn, loc) {
|
|
1374
1486
|
const backendNodeId = await backendNodeIdOf(conn, loc);
|
|
1375
1487
|
await conn.send("DOM.scrollIntoViewIfNeeded", { backendNodeId }).catch(() => {
|
|
1376
1488
|
return;
|
|
@@ -1387,6 +1499,10 @@ async function elementClip(conn, loc) {
|
|
|
1387
1499
|
throw driverError("page-error", "resolved element has zero area");
|
|
1388
1500
|
return { x: minX, y: minY, width, height };
|
|
1389
1501
|
}
|
|
1502
|
+
function documentClip(box, m, devicePx) {
|
|
1503
|
+
const scroll = scrollOffsetCssPx(m, devicePx);
|
|
1504
|
+
return { x: box.x + scroll.x, y: box.y + scroll.y, width: box.width, height: box.height };
|
|
1505
|
+
}
|
|
1390
1506
|
var CDP_CAPABILITIES = new Set([
|
|
1391
1507
|
"trace.performance",
|
|
1392
1508
|
"heap.snapshot",
|
|
@@ -1397,6 +1513,9 @@ var CDP_CAPABILITIES = new Set([
|
|
|
1397
1513
|
"emulate.networkConditions",
|
|
1398
1514
|
"screenshot.fullPage",
|
|
1399
1515
|
"screenshot.element",
|
|
1516
|
+
"screenshot.scale",
|
|
1517
|
+
"screenshot.renderSize",
|
|
1518
|
+
"screenshot.tile",
|
|
1400
1519
|
"network.intercept",
|
|
1401
1520
|
"snapshot.accessibilityTree",
|
|
1402
1521
|
"input.insertTextAtomic",
|
|
@@ -1687,31 +1806,128 @@ class CdpPageDriver {
|
|
|
1687
1806
|
const { objectId } = await resolveElementLocator(this.conn, loc);
|
|
1688
1807
|
await this.conn.send("DOM.setFileInputFiles", { files: [...files], objectId });
|
|
1689
1808
|
}
|
|
1690
|
-
async screenshot(opts) {
|
|
1809
|
+
async screenshot(opts, consumeBands) {
|
|
1810
|
+
const renderSize = opts?.renderWidth !== undefined && opts?.renderHeight !== undefined ? { width: opts.renderWidth, height: opts.renderHeight } : undefined;
|
|
1811
|
+
if (!renderSize)
|
|
1812
|
+
return this.capture(opts, consumeBands);
|
|
1813
|
+
const previous = await readViewportRecord(this.browser.scheme, this.info.id);
|
|
1814
|
+
await this.conn.send("Emulation.setDeviceMetricsOverride", {
|
|
1815
|
+
width: renderSize.width,
|
|
1816
|
+
height: renderSize.height,
|
|
1817
|
+
deviceScaleFactor: previous?.deviceScaleFactor ?? 0,
|
|
1818
|
+
mobile: previous?.mobile ?? false
|
|
1819
|
+
});
|
|
1820
|
+
let shot;
|
|
1821
|
+
let captureError;
|
|
1822
|
+
try {
|
|
1823
|
+
shot = await this.capture(opts, consumeBands);
|
|
1824
|
+
} catch (e) {
|
|
1825
|
+
captureError = e;
|
|
1826
|
+
}
|
|
1827
|
+
let restoreError;
|
|
1828
|
+
try {
|
|
1829
|
+
if (previous) {
|
|
1830
|
+
await this.conn.send("Emulation.setDeviceMetricsOverride", {
|
|
1831
|
+
width: previous.width,
|
|
1832
|
+
height: previous.height,
|
|
1833
|
+
deviceScaleFactor: previous.deviceScaleFactor ?? 0,
|
|
1834
|
+
mobile: previous.mobile ?? false
|
|
1835
|
+
});
|
|
1836
|
+
} else {
|
|
1837
|
+
await this.conn.send("Emulation.clearDeviceMetricsOverride");
|
|
1838
|
+
}
|
|
1839
|
+
} catch (e) {
|
|
1840
|
+
restoreError = e instanceof Error ? e.message : String(e);
|
|
1841
|
+
}
|
|
1842
|
+
if (!shot) {
|
|
1843
|
+
if (restoreError !== undefined && captureError instanceof Error) {
|
|
1844
|
+
captureError.message = `${captureError.message} ${renderRestoreFailureNote(renderSize, restoreError)}`;
|
|
1845
|
+
}
|
|
1846
|
+
throw captureError;
|
|
1847
|
+
}
|
|
1848
|
+
const result = Object.assign(shot, { renderSize, renderRestored: restoreError === undefined });
|
|
1849
|
+
if (restoreError !== undefined)
|
|
1850
|
+
result.renderRestoreError = restoreError;
|
|
1851
|
+
return result;
|
|
1852
|
+
}
|
|
1853
|
+
async capture(opts, consumeBands) {
|
|
1691
1854
|
await this.ensureDomain("Page");
|
|
1692
1855
|
const format = opts?.format ?? "png";
|
|
1693
1856
|
const quality = format === "jpeg" ? opts?.quality ?? 80 : undefined;
|
|
1857
|
+
const scale = opts?.scale ?? 1;
|
|
1858
|
+
const renderSize = opts?.renderWidth !== undefined && opts?.renderHeight !== undefined ? { width: opts.renderWidth, height: opts.renderHeight } : undefined;
|
|
1859
|
+
const box = opts?.clip ? await elementBox(this.conn, opts.clip) : undefined;
|
|
1860
|
+
const metrics = await layoutMetrics(this.conn);
|
|
1861
|
+
const devicePx = devicePixelsPerCssPx(metrics);
|
|
1862
|
+
const clip = box ? documentClip(box, metrics, devicePx) : contentClip(metrics);
|
|
1863
|
+
const sendsClip = opts?.clip !== undefined || opts?.fullPage === true || scale !== 1 || renderSize !== undefined;
|
|
1864
|
+
if (opts?.tile === true && !consumeBands) {
|
|
1865
|
+
throw driverError("unsupported", "screenshot tile:true needs a caller that can consume bands; this one passed no band consumer");
|
|
1866
|
+
}
|
|
1867
|
+
const plan = planTiledCapture(clip, scale, devicePx, consumeBands ? opts?.tile : false);
|
|
1868
|
+
if (plan.tiled)
|
|
1869
|
+
return this.captureTiled(clip, plan, scale, format, consumeBands);
|
|
1870
|
+
assertClipEncodable(clip, scale, devicePx, renderSize);
|
|
1694
1871
|
const params = { format, captureBeyondViewport: true };
|
|
1695
1872
|
if (quality != null)
|
|
1696
1873
|
params.quality = quality;
|
|
1697
|
-
if (
|
|
1698
|
-
params.clip = { ...
|
|
1699
|
-
else if (opts?.fullPage)
|
|
1700
|
-
params.clip = { ...await fullPageClip(this.conn), scale: 1 };
|
|
1874
|
+
if (sendsClip)
|
|
1875
|
+
params.clip = { ...clip, scale };
|
|
1701
1876
|
const { data } = await this.conn.send("Page.captureScreenshot", params);
|
|
1702
1877
|
return { data: new Uint8Array(Buffer.from(data, "base64")), format };
|
|
1703
1878
|
}
|
|
1879
|
+
async captureTiled(clip, plan, scale, format, consumeBands) {
|
|
1880
|
+
const capture = {
|
|
1881
|
+
bands: this.bands(clip, plan, scale, format),
|
|
1882
|
+
bandCount: plan.bandCount,
|
|
1883
|
+
bandHeightCss: plan.bandHeightCss,
|
|
1884
|
+
clip,
|
|
1885
|
+
format: "png"
|
|
1886
|
+
};
|
|
1887
|
+
const consumed = await consumeBands(capture);
|
|
1888
|
+
return { tiled: true, format: "png", bandCount: plan.bandCount, bandHeightCss: plan.bandHeightCss, consumed };
|
|
1889
|
+
}
|
|
1890
|
+
async* bands(clip, plan, scale, format) {
|
|
1891
|
+
if (format !== "png") {
|
|
1892
|
+
throw driverError("unsupported", `a tiled capture is PNG-only (bands are stitched as PNG scanlines); got format "${format}"`);
|
|
1893
|
+
}
|
|
1894
|
+
const bottom = clip.y + clip.height;
|
|
1895
|
+
for (let i = 0;i < plan.bandCount; i++) {
|
|
1896
|
+
const y = clip.y + i * plan.bandHeightCss;
|
|
1897
|
+
const height = Math.min(plan.bandHeightCss, bottom - y);
|
|
1898
|
+
const { data } = await this.conn.send("Page.captureScreenshot", {
|
|
1899
|
+
format: "png",
|
|
1900
|
+
captureBeyondViewport: true,
|
|
1901
|
+
clip: { x: clip.x, y, width: clip.width, height, scale }
|
|
1902
|
+
});
|
|
1903
|
+
yield new Uint8Array(Buffer.from(data, "base64"));
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1704
1906
|
async emulate(opts) {
|
|
1705
1907
|
if (opts.clearOverrides) {
|
|
1706
1908
|
const c = () => {
|
|
1707
1909
|
return;
|
|
1708
1910
|
};
|
|
1709
|
-
|
|
1911
|
+
let metricsClearError;
|
|
1912
|
+
try {
|
|
1913
|
+
await this.ensureDomain("Page");
|
|
1914
|
+
const metrics = await layoutMetrics(this.conn);
|
|
1915
|
+
const width = metrics.cssVisualViewport?.clientWidth ?? metrics.cssLayoutViewport?.clientWidth ?? metrics.visualViewport?.clientWidth ?? metrics.layoutViewport?.clientWidth ?? 1;
|
|
1916
|
+
const height = metrics.cssVisualViewport?.clientHeight ?? metrics.cssLayoutViewport?.clientHeight ?? metrics.visualViewport?.clientHeight ?? metrics.layoutViewport?.clientHeight ?? 1;
|
|
1917
|
+
await this.conn.send("Emulation.setDeviceMetricsOverride", { width, height, deviceScaleFactor: 0, mobile: false });
|
|
1918
|
+
await this.conn.send("Emulation.clearDeviceMetricsOverride");
|
|
1919
|
+
} catch (e) {
|
|
1920
|
+
metricsClearError = e instanceof Error ? e.message : String(e);
|
|
1921
|
+
}
|
|
1710
1922
|
await this.conn.send("Emulation.setUserAgentOverride", { userAgent: "" }).catch(c);
|
|
1711
1923
|
await this.conn.send("Emulation.setCPUThrottlingRate", { rate: 1 }).catch(c);
|
|
1712
1924
|
await this.conn.send("Emulation.setEmulatedMedia", { media: "", features: [] }).catch(c);
|
|
1713
1925
|
await this.ensureDomainSoft("Network");
|
|
1714
1926
|
await this.conn.send("Network.emulateNetworkConditions", { offline: false, latency: 0, downloadThroughput: -1, uploadThroughput: -1 }).catch(c);
|
|
1927
|
+
if (metricsClearError !== undefined) {
|
|
1928
|
+
throw driverError("page-error", `emulate: could not verify the device-metrics override was cleared (${metricsClearError})`);
|
|
1929
|
+
}
|
|
1930
|
+
await clearViewportRecord(this.browser.scheme, this.info.id);
|
|
1715
1931
|
return { applied: [] };
|
|
1716
1932
|
}
|
|
1717
1933
|
const applied = [];
|
|
@@ -1725,6 +1941,12 @@ class CdpPageDriver {
|
|
|
1725
1941
|
deviceScaleFactor: opts.deviceScaleFactor ?? 0,
|
|
1726
1942
|
mobile: opts.mobile ?? false
|
|
1727
1943
|
});
|
|
1944
|
+
await writeViewportRecord(this.browser.scheme, this.info.id, {
|
|
1945
|
+
width: opts.width,
|
|
1946
|
+
height: opts.height,
|
|
1947
|
+
deviceScaleFactor: opts.deviceScaleFactor ?? 0,
|
|
1948
|
+
mobile: opts.mobile ?? false
|
|
1949
|
+
});
|
|
1728
1950
|
applied.push("deviceMetrics");
|
|
1729
1951
|
}
|
|
1730
1952
|
if (opts.userAgent != null) {
|
|
@@ -2112,8 +2334,522 @@ async function reapStaleAgentTabs(driver, backend) {
|
|
|
2112
2334
|
}
|
|
2113
2335
|
|
|
2114
2336
|
// src/shared-tools.ts
|
|
2115
|
-
import { mkdir as
|
|
2116
|
-
import { dirname, join as
|
|
2337
|
+
import { mkdir as mkdir5, writeFile as writeFile4 } from "node:fs/promises";
|
|
2338
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
2339
|
+
|
|
2340
|
+
// src/png.ts
|
|
2341
|
+
import { once } from "node:events";
|
|
2342
|
+
import { mkdir as mkdir4, open, rename, rm } from "node:fs/promises";
|
|
2343
|
+
import { dirname } from "node:path";
|
|
2344
|
+
import { createDeflate, createInflate, deflateSync } from "node:zlib";
|
|
2345
|
+
var PNG_SIGNATURE = Uint8Array.of(137, 80, 78, 71, 13, 10, 26, 10);
|
|
2346
|
+
var ZLIB_CHUNK = 1 << 18;
|
|
2347
|
+
var BLOCK_RAW_BYTES = 1 << 22;
|
|
2348
|
+
var MAX_BLOCK_ROWS = 4096;
|
|
2349
|
+
var DECIDE_LEVEL = 6;
|
|
2350
|
+
var CARRIED_CHUNKS = ["gAMA", "cHRM", "sRGB", "iCCP", "pHYs"];
|
|
2351
|
+
var CHANNELS = { 0: 1, 2: 3, 4: 2, 6: 4 };
|
|
2352
|
+
var LEGAL_DEPTHS = {
|
|
2353
|
+
0: [1, 2, 4, 8, 16],
|
|
2354
|
+
2: [8, 16],
|
|
2355
|
+
4: [8, 16],
|
|
2356
|
+
6: [8, 16]
|
|
2357
|
+
};
|
|
2358
|
+
var COLOUR_TYPE_NAMES = {
|
|
2359
|
+
0: "greyscale",
|
|
2360
|
+
2: "truecolour RGB",
|
|
2361
|
+
3: "palette",
|
|
2362
|
+
4: "greyscale+alpha",
|
|
2363
|
+
6: "RGBA"
|
|
2364
|
+
};
|
|
2365
|
+
|
|
2366
|
+
class PngStitchError extends Error {
|
|
2367
|
+
constructor(message) {
|
|
2368
|
+
super(message);
|
|
2369
|
+
this.name = "PngStitchError";
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
var CRC_TABLE = (() => {
|
|
2373
|
+
const table = new Uint32Array(256);
|
|
2374
|
+
for (let n = 0;n < 256; n++) {
|
|
2375
|
+
let c = n;
|
|
2376
|
+
for (let k = 0;k < 8; k++)
|
|
2377
|
+
c = (c & 1) !== 0 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
2378
|
+
table[n] = c >>> 0;
|
|
2379
|
+
}
|
|
2380
|
+
return table;
|
|
2381
|
+
})();
|
|
2382
|
+
function crcUpdate(crc, bytes) {
|
|
2383
|
+
let c = crc;
|
|
2384
|
+
for (let i = 0;i < bytes.length; i++)
|
|
2385
|
+
c = CRC_TABLE[(c ^ bytes[i]) & 255] ^ c >>> 8;
|
|
2386
|
+
return c >>> 0;
|
|
2387
|
+
}
|
|
2388
|
+
function crc32(bytes) {
|
|
2389
|
+
return (crcUpdate(4294967295, bytes) ^ 4294967295) >>> 0;
|
|
2390
|
+
}
|
|
2391
|
+
function readU32(b, off) {
|
|
2392
|
+
return (b[off] << 24 | b[off + 1] << 16 | b[off + 2] << 8 | b[off + 3]) >>> 0;
|
|
2393
|
+
}
|
|
2394
|
+
function chunkTypeAt(b, off) {
|
|
2395
|
+
return String.fromCharCode(b[off], b[off + 1], b[off + 2], b[off + 3]);
|
|
2396
|
+
}
|
|
2397
|
+
function sameBytes(a, b) {
|
|
2398
|
+
if (a.length !== b.length)
|
|
2399
|
+
return false;
|
|
2400
|
+
for (let i = 0;i < a.length; i++)
|
|
2401
|
+
if (a[i] !== b[i])
|
|
2402
|
+
return false;
|
|
2403
|
+
return true;
|
|
2404
|
+
}
|
|
2405
|
+
function paeth(a, b, c) {
|
|
2406
|
+
const p = a + b - c;
|
|
2407
|
+
const pa = p > a ? p - a : a - p;
|
|
2408
|
+
const pb = p > b ? p - b : b - p;
|
|
2409
|
+
const pc = p > c ? p - c : c - p;
|
|
2410
|
+
if (pa <= pb && pa <= pc)
|
|
2411
|
+
return a;
|
|
2412
|
+
return pb <= pc ? b : c;
|
|
2413
|
+
}
|
|
2414
|
+
function unfilterRow(type, row, prev, bpp, at) {
|
|
2415
|
+
const n = row.length;
|
|
2416
|
+
switch (type) {
|
|
2417
|
+
case 0:
|
|
2418
|
+
return;
|
|
2419
|
+
case 1:
|
|
2420
|
+
for (let i = bpp;i < n; i++)
|
|
2421
|
+
row[i] = row[i] + row[i - bpp] & 255;
|
|
2422
|
+
return;
|
|
2423
|
+
case 2:
|
|
2424
|
+
for (let i = 0;i < n; i++)
|
|
2425
|
+
row[i] = row[i] + prev[i] & 255;
|
|
2426
|
+
return;
|
|
2427
|
+
case 3:
|
|
2428
|
+
for (let i = 0;i < n; i++) {
|
|
2429
|
+
const a = i >= bpp ? row[i - bpp] : 0;
|
|
2430
|
+
row[i] = row[i] + (a + prev[i] >> 1) & 255;
|
|
2431
|
+
}
|
|
2432
|
+
return;
|
|
2433
|
+
case 4:
|
|
2434
|
+
for (let i = 0;i < n; i++) {
|
|
2435
|
+
const a = i >= bpp ? row[i - bpp] : 0;
|
|
2436
|
+
const c = i >= bpp ? prev[i - bpp] : 0;
|
|
2437
|
+
row[i] = row[i] + paeth(a, prev[i], c) & 255;
|
|
2438
|
+
}
|
|
2439
|
+
return;
|
|
2440
|
+
default:
|
|
2441
|
+
throw new PngStitchError(`${at}: unknown row filter type ${type} (PNG defines 0-4)`);
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
function absSigned(v) {
|
|
2445
|
+
const u = v & 255;
|
|
2446
|
+
return u < 128 ? u : 256 - u;
|
|
2447
|
+
}
|
|
2448
|
+
function chooseFilter(row, prev, bpp) {
|
|
2449
|
+
const n = row.length;
|
|
2450
|
+
let s0 = 0, s1 = 0, s2 = 0, s3 = 0, s4 = 0;
|
|
2451
|
+
for (let i = 0;i < n; i++) {
|
|
2452
|
+
const x = row[i];
|
|
2453
|
+
const b = prev[i];
|
|
2454
|
+
const a = i >= bpp ? row[i - bpp] : 0;
|
|
2455
|
+
const c = i >= bpp ? prev[i - bpp] : 0;
|
|
2456
|
+
s0 += absSigned(x);
|
|
2457
|
+
s1 += absSigned(x - a);
|
|
2458
|
+
s2 += absSigned(x - b);
|
|
2459
|
+
s3 += absSigned(x - (a + b >> 1));
|
|
2460
|
+
s4 += absSigned(x - paeth(a, b, c));
|
|
2461
|
+
}
|
|
2462
|
+
let best = 0, bestSum = s0;
|
|
2463
|
+
if (s1 < bestSum) {
|
|
2464
|
+
best = 1;
|
|
2465
|
+
bestSum = s1;
|
|
2466
|
+
}
|
|
2467
|
+
if (s2 < bestSum) {
|
|
2468
|
+
best = 2;
|
|
2469
|
+
bestSum = s2;
|
|
2470
|
+
}
|
|
2471
|
+
if (s3 < bestSum) {
|
|
2472
|
+
best = 3;
|
|
2473
|
+
bestSum = s3;
|
|
2474
|
+
}
|
|
2475
|
+
if (s4 < bestSum) {
|
|
2476
|
+
best = 4;
|
|
2477
|
+
}
|
|
2478
|
+
return best;
|
|
2479
|
+
}
|
|
2480
|
+
function writeFilteredRow(type, row, prev, bpp, out, off) {
|
|
2481
|
+
const n = row.length;
|
|
2482
|
+
switch (type) {
|
|
2483
|
+
case 0:
|
|
2484
|
+
out.set(row, off);
|
|
2485
|
+
return;
|
|
2486
|
+
case 1:
|
|
2487
|
+
for (let i = 0;i < n; i++)
|
|
2488
|
+
out[off + i] = row[i] - (i >= bpp ? row[i - bpp] : 0) & 255;
|
|
2489
|
+
return;
|
|
2490
|
+
case 2:
|
|
2491
|
+
for (let i = 0;i < n; i++)
|
|
2492
|
+
out[off + i] = row[i] - prev[i] & 255;
|
|
2493
|
+
return;
|
|
2494
|
+
case 3:
|
|
2495
|
+
for (let i = 0;i < n; i++) {
|
|
2496
|
+
const a = i >= bpp ? row[i - bpp] : 0;
|
|
2497
|
+
out[off + i] = row[i] - (a + prev[i] >> 1) & 255;
|
|
2498
|
+
}
|
|
2499
|
+
return;
|
|
2500
|
+
default:
|
|
2501
|
+
for (let i = 0;i < n; i++) {
|
|
2502
|
+
const a = i >= bpp ? row[i - bpp] : 0;
|
|
2503
|
+
const c = i >= bpp ? prev[i - bpp] : 0;
|
|
2504
|
+
out[off + i] = row[i] - paeth(a, prev[i], c) & 255;
|
|
2505
|
+
}
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
function filterUnitBytes(h) {
|
|
2510
|
+
return Math.max(1, Math.ceil(CHANNELS[h.colourType] * h.bitDepth / 8));
|
|
2511
|
+
}
|
|
2512
|
+
function bytesPerRow(h) {
|
|
2513
|
+
return Math.ceil(h.width * CHANNELS[h.colourType] * h.bitDepth / 8);
|
|
2514
|
+
}
|
|
2515
|
+
function readIhdr(band, dataStart, at) {
|
|
2516
|
+
const width = readU32(band, dataStart);
|
|
2517
|
+
const height = readU32(band, dataStart + 4);
|
|
2518
|
+
const bitDepth = band[dataStart + 8];
|
|
2519
|
+
const colourType = band[dataStart + 9];
|
|
2520
|
+
const compression = band[dataStart + 10];
|
|
2521
|
+
const filterMethod = band[dataStart + 11];
|
|
2522
|
+
const interlace = band[dataStart + 12];
|
|
2523
|
+
if (width < 1 || height < 1)
|
|
2524
|
+
throw new PngStitchError(`${at}: IHDR declares ${width}x${height}; both dimensions must be >= 1`);
|
|
2525
|
+
if (compression !== 0)
|
|
2526
|
+
throw new PngStitchError(`${at}: IHDR compression method ${compression}; PNG defines only 0 (deflate)`);
|
|
2527
|
+
if (filterMethod !== 0)
|
|
2528
|
+
throw new PngStitchError(`${at}: IHDR filter method ${filterMethod}; PNG defines only 0`);
|
|
2529
|
+
if (interlace === 1) {
|
|
2530
|
+
throw new PngStitchError(`${at}: interlaced (Adam7) PNG is not supported. Its seven reduced passes are not scanlines of the final image, ` + `so bands of them cannot be concatenated; re-capture without interlacing (Chrome's Page.captureScreenshot always does).`);
|
|
2531
|
+
}
|
|
2532
|
+
if (interlace !== 0)
|
|
2533
|
+
throw new PngStitchError(`${at}: IHDR interlace method ${interlace}; PNG defines only 0 (none) and 1 (Adam7)`);
|
|
2534
|
+
if (colourType === 3) {
|
|
2535
|
+
throw new PngStitchError(`${at}: colour type 3 (palette) is not supported. Concatenating palettized bands requires every band's PLTE/tRNS to ` + `be identical or a full palette remap; Chrome emits colour type 2, so this path is refused rather than guessed.`);
|
|
2536
|
+
}
|
|
2537
|
+
if (CHANNELS[colourType] === undefined)
|
|
2538
|
+
throw new PngStitchError(`${at}: IHDR colour type ${colourType}; PNG defines 0, 2, 3, 4 and 6`);
|
|
2539
|
+
if (!LEGAL_DEPTHS[colourType].includes(bitDepth)) {
|
|
2540
|
+
throw new PngStitchError(`${at}: bit depth ${bitDepth} is not legal for colour type ${colourType} (${COLOUR_TYPE_NAMES[colourType]}); ` + `allowed: ${LEGAL_DEPTHS[colourType].join(", ")}`);
|
|
2541
|
+
}
|
|
2542
|
+
return { width, height, bitDepth, colourType };
|
|
2543
|
+
}
|
|
2544
|
+
function parseBand(band, index) {
|
|
2545
|
+
const at = `band ${index}`;
|
|
2546
|
+
if (band.length < 8)
|
|
2547
|
+
throw new PngStitchError(`${at}: ${band.length} bytes is too short to be a PNG`);
|
|
2548
|
+
for (let i = 0;i < 8; i++) {
|
|
2549
|
+
if (band[i] !== PNG_SIGNATURE[i])
|
|
2550
|
+
throw new PngStitchError(`${at}: not a PNG (bad 8-byte signature)`);
|
|
2551
|
+
}
|
|
2552
|
+
let header;
|
|
2553
|
+
const idat = [];
|
|
2554
|
+
const carried = [];
|
|
2555
|
+
let sawIend = false;
|
|
2556
|
+
let off = 8;
|
|
2557
|
+
while (off + 8 <= band.length) {
|
|
2558
|
+
const len = readU32(band, off);
|
|
2559
|
+
const type = chunkTypeAt(band, off + 4);
|
|
2560
|
+
const dataStart = off + 8;
|
|
2561
|
+
const dataEnd = dataStart + len;
|
|
2562
|
+
if (dataEnd + 4 > band.length) {
|
|
2563
|
+
throw new PngStitchError(`${at}: chunk "${type}" at byte ${off} claims ${len} payload bytes but only ${band.length - dataStart} remain (truncated PNG)`);
|
|
2564
|
+
}
|
|
2565
|
+
if (type === "IHDR") {
|
|
2566
|
+
if (off !== 8)
|
|
2567
|
+
throw new PngStitchError(`${at}: IHDR must be the first chunk, found it at byte ${off}`);
|
|
2568
|
+
if (len !== 13)
|
|
2569
|
+
throw new PngStitchError(`${at}: IHDR payload is ${len} bytes, must be 13`);
|
|
2570
|
+
const want = readU32(band, dataEnd);
|
|
2571
|
+
const got = crc32(band.subarray(off + 4, dataEnd));
|
|
2572
|
+
if (want !== got)
|
|
2573
|
+
throw new PngStitchError(`${at}: IHDR CRC mismatch (file says 0x${want.toString(16)}, computed 0x${got.toString(16)})`);
|
|
2574
|
+
header = readIhdr(band, dataStart, at);
|
|
2575
|
+
} else if (!header) {
|
|
2576
|
+
throw new PngStitchError(`${at}: first chunk is "${type}", expected IHDR`);
|
|
2577
|
+
} else if (type === "IDAT") {
|
|
2578
|
+
idat.push([dataStart, dataEnd]);
|
|
2579
|
+
} else if (type === "IEND") {
|
|
2580
|
+
sawIend = true;
|
|
2581
|
+
break;
|
|
2582
|
+
} else if (CARRIED_CHUNKS.includes(type)) {
|
|
2583
|
+
carried.push({ type, data: band.subarray(dataStart, dataEnd) });
|
|
2584
|
+
}
|
|
2585
|
+
off = dataEnd + 4;
|
|
2586
|
+
}
|
|
2587
|
+
if (!header)
|
|
2588
|
+
throw new PngStitchError(`${at}: no IHDR chunk`);
|
|
2589
|
+
if (idat.length === 0)
|
|
2590
|
+
throw new PngStitchError(`${at}: no IDAT chunk`);
|
|
2591
|
+
if (!sawIend)
|
|
2592
|
+
throw new PngStitchError(`${at}: no IEND chunk (truncated PNG)`);
|
|
2593
|
+
return { header, idat, carried };
|
|
2594
|
+
}
|
|
2595
|
+
function assertCompatible(first, next, firstCarried, nextCarried, index) {
|
|
2596
|
+
const at = `band ${index}`;
|
|
2597
|
+
if (next.width !== first.width)
|
|
2598
|
+
throw new PngStitchError(`${at}: width ${next.width} does not match band 1's width ${first.width}`);
|
|
2599
|
+
if (next.bitDepth !== first.bitDepth)
|
|
2600
|
+
throw new PngStitchError(`${at}: bit depth ${next.bitDepth} does not match band 1's bit depth ${first.bitDepth}`);
|
|
2601
|
+
if (next.colourType !== first.colourType) {
|
|
2602
|
+
throw new PngStitchError(`${at}: colour type ${next.colourType} (${COLOUR_TYPE_NAMES[next.colourType] ?? "?"}) does not match band 1's ` + `colour type ${first.colourType} (${COLOUR_TYPE_NAMES[first.colourType] ?? "?"})`);
|
|
2603
|
+
}
|
|
2604
|
+
for (const type of CARRIED_CHUNKS) {
|
|
2605
|
+
const a = firstCarried.find((c) => c.type === type);
|
|
2606
|
+
const b = nextCarried.find((c) => c.type === type);
|
|
2607
|
+
if (!a && !b)
|
|
2608
|
+
continue;
|
|
2609
|
+
if (!a || !b)
|
|
2610
|
+
throw new PngStitchError(`${at}: ${b ? "has" : "is missing"} a "${type}" chunk that band 1 ${b ? "does not have" : "has"}; the bands disagree about how their samples are interpreted`);
|
|
2611
|
+
if (!sameBytes(a.data, b.data))
|
|
2612
|
+
throw new PngStitchError(`${at}: its "${type}" chunk differs from band 1's; the bands disagree about how their samples are interpreted`);
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
class ChunkWriter {
|
|
2617
|
+
fh;
|
|
2618
|
+
pos = 0;
|
|
2619
|
+
constructor(fh) {
|
|
2620
|
+
this.fh = fh;
|
|
2621
|
+
}
|
|
2622
|
+
get bytes() {
|
|
2623
|
+
return this.pos;
|
|
2624
|
+
}
|
|
2625
|
+
async writeRaw(b) {
|
|
2626
|
+
await this.fh.write(b, 0, b.length, this.pos);
|
|
2627
|
+
this.pos += b.length;
|
|
2628
|
+
}
|
|
2629
|
+
async writeChunk(type, payload) {
|
|
2630
|
+
const head = Buffer.allocUnsafe(8);
|
|
2631
|
+
head.writeUInt32BE(payload.length, 0);
|
|
2632
|
+
head.write(type, 4, "latin1");
|
|
2633
|
+
const crc = (crcUpdate(crcUpdate(4294967295, head.subarray(4)), payload) ^ 4294967295) >>> 0;
|
|
2634
|
+
const tail = Buffer.allocUnsafe(4);
|
|
2635
|
+
tail.writeUInt32BE(crc, 0);
|
|
2636
|
+
await this.fh.writev([head, payload, tail], this.pos);
|
|
2637
|
+
this.pos += 8 + payload.length + 4;
|
|
2638
|
+
}
|
|
2639
|
+
async patchAt(position, b) {
|
|
2640
|
+
await this.fh.write(b, 0, b.length, position);
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
function ihdrChunk(h, height) {
|
|
2644
|
+
const buf = Buffer.allocUnsafe(25);
|
|
2645
|
+
buf.writeUInt32BE(13, 0);
|
|
2646
|
+
buf.write("IHDR", 4, "latin1");
|
|
2647
|
+
buf.writeUInt32BE(h.width, 8);
|
|
2648
|
+
buf.writeUInt32BE(height, 12);
|
|
2649
|
+
buf[16] = h.bitDepth;
|
|
2650
|
+
buf[17] = h.colourType;
|
|
2651
|
+
buf[18] = 0;
|
|
2652
|
+
buf[19] = 0;
|
|
2653
|
+
buf[20] = 0;
|
|
2654
|
+
buf.writeUInt32BE(crc32(buf.subarray(4, 21)), 21);
|
|
2655
|
+
return buf;
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
class RowEncoder {
|
|
2659
|
+
writer;
|
|
2660
|
+
bpr;
|
|
2661
|
+
bpp;
|
|
2662
|
+
deflate = createDeflate({ chunkSize: ZLIB_CHUNK });
|
|
2663
|
+
pump;
|
|
2664
|
+
rowsPerBlock;
|
|
2665
|
+
block;
|
|
2666
|
+
blockRows = 0;
|
|
2667
|
+
candNone;
|
|
2668
|
+
candHeuristic;
|
|
2669
|
+
prevRaw;
|
|
2670
|
+
constructor(writer, bpr, bpp) {
|
|
2671
|
+
this.writer = writer;
|
|
2672
|
+
this.bpr = bpr;
|
|
2673
|
+
this.bpp = bpp;
|
|
2674
|
+
this.rowsPerBlock = Math.min(MAX_BLOCK_ROWS, Math.max(1, Math.floor(BLOCK_RAW_BYTES / bpr)));
|
|
2675
|
+
this.block = Buffer.allocUnsafe(this.rowsPerBlock * bpr);
|
|
2676
|
+
this.candNone = Buffer.allocUnsafe(this.rowsPerBlock * (bpr + 1));
|
|
2677
|
+
this.candHeuristic = Buffer.allocUnsafe(this.rowsPerBlock * (bpr + 1));
|
|
2678
|
+
this.prevRaw = new Uint8Array(bpr);
|
|
2679
|
+
this.pump = (async () => {
|
|
2680
|
+
for await (const chunk of this.deflate)
|
|
2681
|
+
await this.writer.writeChunk("IDAT", chunk);
|
|
2682
|
+
})();
|
|
2683
|
+
this.pump.catch(() => {});
|
|
2684
|
+
}
|
|
2685
|
+
async addRow(raw) {
|
|
2686
|
+
this.block.set(raw, this.blockRows * this.bpr);
|
|
2687
|
+
this.blockRows += 1;
|
|
2688
|
+
if (this.blockRows === this.rowsPerBlock)
|
|
2689
|
+
await this.emitBlock();
|
|
2690
|
+
}
|
|
2691
|
+
async emitBlock() {
|
|
2692
|
+
const n = this.blockRows;
|
|
2693
|
+
if (n === 0)
|
|
2694
|
+
return;
|
|
2695
|
+
const { bpr, bpp } = this;
|
|
2696
|
+
const stride = bpr + 1;
|
|
2697
|
+
let prev = this.prevRaw;
|
|
2698
|
+
for (let r = 0;r < n; r++) {
|
|
2699
|
+
const row = this.block.subarray(r * bpr, (r + 1) * bpr);
|
|
2700
|
+
const at = r * stride;
|
|
2701
|
+
this.candNone[at] = 0;
|
|
2702
|
+
this.candNone.set(row, at + 1);
|
|
2703
|
+
const filter = chooseFilter(row, prev, bpp);
|
|
2704
|
+
this.candHeuristic[at] = filter;
|
|
2705
|
+
writeFilteredRow(filter, row, prev, bpp, this.candHeuristic, at + 1);
|
|
2706
|
+
prev = row;
|
|
2707
|
+
}
|
|
2708
|
+
const used = n * stride;
|
|
2709
|
+
const a = this.candNone.subarray(0, used);
|
|
2710
|
+
const b = this.candHeuristic.subarray(0, used);
|
|
2711
|
+
const heuristicWins = deflateSync(b, { level: DECIDE_LEVEL }).length < deflateSync(a, { level: DECIDE_LEVEL }).length;
|
|
2712
|
+
this.prevRaw.set(prev);
|
|
2713
|
+
this.blockRows = 0;
|
|
2714
|
+
const out = Buffer.from(heuristicWins ? b : a);
|
|
2715
|
+
if (!this.deflate.write(out))
|
|
2716
|
+
await once(this.deflate, "drain");
|
|
2717
|
+
}
|
|
2718
|
+
async finish() {
|
|
2719
|
+
await this.emitBlock();
|
|
2720
|
+
this.deflate.end();
|
|
2721
|
+
await this.pump;
|
|
2722
|
+
}
|
|
2723
|
+
async abort() {
|
|
2724
|
+
this.deflate.destroy();
|
|
2725
|
+
await this.pump.catch(() => {});
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
async function* inflateBandIdat(band, slices, at) {
|
|
2729
|
+
const inf = createInflate({ chunkSize: ZLIB_CHUNK });
|
|
2730
|
+
const feed = (async () => {
|
|
2731
|
+
for (const [start, end] of slices) {
|
|
2732
|
+
const view = Buffer.from(band.buffer, band.byteOffset + start, end - start);
|
|
2733
|
+
if (!inf.write(view))
|
|
2734
|
+
await once(inf, "drain");
|
|
2735
|
+
}
|
|
2736
|
+
inf.end();
|
|
2737
|
+
})();
|
|
2738
|
+
feed.catch(() => {});
|
|
2739
|
+
try {
|
|
2740
|
+
for await (const chunk of inf)
|
|
2741
|
+
yield chunk;
|
|
2742
|
+
} catch (err) {
|
|
2743
|
+
throw new PngStitchError(`${at}: IDAT stream would not inflate (${err.message})`);
|
|
2744
|
+
} finally {
|
|
2745
|
+
inf.destroy();
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
function isAsyncIterable(x) {
|
|
2749
|
+
return typeof x === "object" && x !== null && Symbol.asyncIterator in x;
|
|
2750
|
+
}
|
|
2751
|
+
async function* eachBand(bands) {
|
|
2752
|
+
if (isAsyncIterable(bands)) {
|
|
2753
|
+
for await (const b of bands)
|
|
2754
|
+
yield b;
|
|
2755
|
+
} else {
|
|
2756
|
+
for (const b of bands)
|
|
2757
|
+
yield b;
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
async function stitchPngBandsToFile(bands, outPath) {
|
|
2761
|
+
await mkdir4(dirname(outPath), { recursive: true });
|
|
2762
|
+
const partPath = `${outPath}.stitch-partial`;
|
|
2763
|
+
const fh = await open(partPath, "w");
|
|
2764
|
+
let done = false;
|
|
2765
|
+
try {
|
|
2766
|
+
const result = await stitchInto(fh, bands);
|
|
2767
|
+
await fh.close();
|
|
2768
|
+
await rename(partPath, outPath);
|
|
2769
|
+
done = true;
|
|
2770
|
+
return result;
|
|
2771
|
+
} finally {
|
|
2772
|
+
if (!done) {
|
|
2773
|
+
await fh.close().catch(() => {});
|
|
2774
|
+
await rm(partPath, { force: true }).catch(() => {});
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
async function stitchInto(fh, bands) {
|
|
2779
|
+
const writer = new ChunkWriter(fh);
|
|
2780
|
+
let first;
|
|
2781
|
+
let firstCarried = [];
|
|
2782
|
+
let encoder;
|
|
2783
|
+
let prevRow = new Uint8Array(0);
|
|
2784
|
+
let rowBuf = new Uint8Array(0);
|
|
2785
|
+
let bpp = 0;
|
|
2786
|
+
let bpr = 0;
|
|
2787
|
+
let totalRows = 0;
|
|
2788
|
+
let index = 0;
|
|
2789
|
+
try {
|
|
2790
|
+
for await (const band of eachBand(bands)) {
|
|
2791
|
+
index += 1;
|
|
2792
|
+
const parsed = parseBand(band, index);
|
|
2793
|
+
const at = `band ${index}`;
|
|
2794
|
+
if (!first) {
|
|
2795
|
+
first = parsed.header;
|
|
2796
|
+
firstCarried = parsed.carried;
|
|
2797
|
+
bpp = filterUnitBytes(first);
|
|
2798
|
+
bpr = bytesPerRow(first);
|
|
2799
|
+
prevRow = new Uint8Array(bpr);
|
|
2800
|
+
rowBuf = new Uint8Array(1 + bpr);
|
|
2801
|
+
await writer.writeRaw(PNG_SIGNATURE);
|
|
2802
|
+
await writer.writeRaw(ihdrChunk(first, 1));
|
|
2803
|
+
for (const c of parsed.carried)
|
|
2804
|
+
await writer.writeChunk(c.type, c.data);
|
|
2805
|
+
encoder = new RowEncoder(writer, bpr, bpp);
|
|
2806
|
+
} else {
|
|
2807
|
+
assertCompatible(first, parsed.header, firstCarried, parsed.carried, index);
|
|
2808
|
+
}
|
|
2809
|
+
const enc = encoder;
|
|
2810
|
+
const raw = rowBuf.subarray(1);
|
|
2811
|
+
prevRow.fill(0);
|
|
2812
|
+
let rows = 0;
|
|
2813
|
+
let filled = 0;
|
|
2814
|
+
for await (const chunk of inflateBandIdat(band, parsed.idat, at)) {
|
|
2815
|
+
let off = 0;
|
|
2816
|
+
while (off < chunk.length) {
|
|
2817
|
+
const take = Math.min(rowBuf.length - filled, chunk.length - off);
|
|
2818
|
+
rowBuf.set(chunk.subarray(off, off + take), filled);
|
|
2819
|
+
filled += take;
|
|
2820
|
+
off += take;
|
|
2821
|
+
if (filled !== rowBuf.length)
|
|
2822
|
+
continue;
|
|
2823
|
+
if (rows >= parsed.header.height) {
|
|
2824
|
+
throw new PngStitchError(`${at}: IDAT holds more scanlines than its IHDR height of ${parsed.header.height}`);
|
|
2825
|
+
}
|
|
2826
|
+
unfilterRow(rowBuf[0], raw, prevRow, bpp, at);
|
|
2827
|
+
await enc.addRow(raw);
|
|
2828
|
+
prevRow.set(raw);
|
|
2829
|
+
rows += 1;
|
|
2830
|
+
filled = 0;
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
if (filled !== 0 || rows !== parsed.header.height) {
|
|
2834
|
+
throw new PngStitchError(`${at}: IDAT decoded to ${rows} complete scanlines${filled > 0 ? ` plus ${filled} trailing bytes` : ""}, but its IHDR declares ${parsed.header.height}`);
|
|
2835
|
+
}
|
|
2836
|
+
totalRows += parsed.header.height;
|
|
2837
|
+
if (totalRows > 2147483647)
|
|
2838
|
+
throw new PngStitchError(`stitched height would be ${totalRows}, past PNG's 2^31-1 limit`);
|
|
2839
|
+
}
|
|
2840
|
+
if (!first || !encoder)
|
|
2841
|
+
throw new PngStitchError("no bands to stitch");
|
|
2842
|
+
await encoder.finish();
|
|
2843
|
+
await writer.writeChunk("IEND", new Uint8Array(0));
|
|
2844
|
+
await writer.patchAt(8, ihdrChunk(first, totalRows));
|
|
2845
|
+
return { width: first.width, height: totalRows, bytes: writer.bytes, bands: index };
|
|
2846
|
+
} catch (err) {
|
|
2847
|
+
await encoder?.abort();
|
|
2848
|
+
throw err;
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
// src/shared-tools.ts
|
|
2117
2853
|
var ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2118
2854
|
var STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
2119
2855
|
|
|
@@ -2258,8 +2994,8 @@ async function closePage(driver, args) {
|
|
|
2258
2994
|
return { closed: p.id, success: true, ...released.released ? { leaseReleased: true } : {} };
|
|
2259
2995
|
}
|
|
2260
2996
|
async function writeSelectedFile(id) {
|
|
2261
|
-
await
|
|
2262
|
-
await
|
|
2997
|
+
await mkdir5(STATE_DIR, { recursive: true });
|
|
2998
|
+
await writeFile4(join4(STATE_DIR, "selected"), id, "utf8");
|
|
2263
2999
|
}
|
|
2264
3000
|
async function selectPage(driver, args) {
|
|
2265
3001
|
if (args.target === undefined || args.target === "")
|
|
@@ -2323,10 +3059,10 @@ function serializeForSink(value) {
|
|
|
2323
3059
|
return json ?? "null";
|
|
2324
3060
|
}
|
|
2325
3061
|
async function writeJsonSink(savePath, value) {
|
|
2326
|
-
const path = savePath.startsWith("/") ? savePath :
|
|
2327
|
-
await
|
|
3062
|
+
const path = savePath.startsWith("/") ? savePath : join4(ARTIFACT_DIR, savePath);
|
|
3063
|
+
await mkdir5(dirname2(path), { recursive: true });
|
|
2328
3064
|
const json = serializeForSink(value);
|
|
2329
|
-
await
|
|
3065
|
+
await writeFile4(path, json, "utf8");
|
|
2330
3066
|
return { path, bytes: Buffer.byteLength(json, "utf8") };
|
|
2331
3067
|
}
|
|
2332
3068
|
function sinkRequested(savePath) {
|
|
@@ -2630,30 +3366,166 @@ async function uploadFile(driver, args) {
|
|
|
2630
3366
|
function stamp() {
|
|
2631
3367
|
return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
|
|
2632
3368
|
}
|
|
3369
|
+
var MAX_SCREENSHOT_SCALE = 8;
|
|
3370
|
+
function resolveScreenshotScale(scale, capabilities) {
|
|
3371
|
+
if (scale === undefined)
|
|
3372
|
+
return 1;
|
|
3373
|
+
if (typeof scale !== "number" || !Number.isFinite(scale) || scale <= 0 || scale > MAX_SCREENSHOT_SCALE) {
|
|
3374
|
+
throw new SharedToolError(`take_screenshot: 'scale' must be a finite number greater than 0 and at most ${MAX_SCREENSHOT_SCALE} (default 1)`);
|
|
3375
|
+
}
|
|
3376
|
+
if (scale !== 1 && !capabilities.has("screenshot.scale")) {
|
|
3377
|
+
throw new SharedToolError("take_screenshot: 'scale' is not supported by this backend (WebDriver BiDi's browsingContext.captureScreenshot has no scale parameter). " + `Call emulate with deviceScaleFactor:${scale} first and capture at scale 1, or use --browser chrome for a per-capture scale.`);
|
|
3378
|
+
}
|
|
3379
|
+
return scale;
|
|
3380
|
+
}
|
|
3381
|
+
var MAX_RENDER_SIZE_PX = 16384;
|
|
3382
|
+
function resolveRenderSize(renderWidth, renderHeight, capabilities) {
|
|
3383
|
+
if (renderWidth === undefined && renderHeight === undefined)
|
|
3384
|
+
return;
|
|
3385
|
+
if (renderWidth === undefined || renderHeight === undefined) {
|
|
3386
|
+
throw new SharedToolError("take_screenshot: 'renderWidth' and 'renderHeight' are required together (a viewport is two numbers; " + `got ${renderWidth === undefined ? "only renderHeight" : "only renderWidth"})`);
|
|
3387
|
+
}
|
|
3388
|
+
for (const [name, value] of [["renderWidth", renderWidth], ["renderHeight", renderHeight]]) {
|
|
3389
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > MAX_RENDER_SIZE_PX) {
|
|
3390
|
+
throw new SharedToolError(`take_screenshot: '${name}' must be an integer between 1 and ${MAX_RENDER_SIZE_PX} css px (got ${JSON.stringify(value)})`);
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
if (!capabilities.has("screenshot.renderSize")) {
|
|
3394
|
+
throw new SharedToolError("take_screenshot: 'renderWidth'/'renderHeight' are not supported by this backend (it cannot emulate a viewport for a capture). " + `Call emulate with width:${renderWidth}, height:${renderHeight} first, capture, then restore the viewport yourself.`);
|
|
3395
|
+
}
|
|
3396
|
+
return { width: renderWidth, height: renderHeight };
|
|
3397
|
+
}
|
|
3398
|
+
function resolveTileMode(tile, capabilities, format, returnBase64) {
|
|
3399
|
+
if (tile === undefined)
|
|
3400
|
+
return;
|
|
3401
|
+
if (typeof tile !== "boolean")
|
|
3402
|
+
throw new SharedToolError(`take_screenshot: 'tile' must be true, false, or omitted for automatic (got ${JSON.stringify(tile)})`);
|
|
3403
|
+
if (!tile)
|
|
3404
|
+
return false;
|
|
3405
|
+
if (!capabilities.has("screenshot.tile")) {
|
|
3406
|
+
throw new SharedToolError("take_screenshot: 'tile' is not supported by this backend (band tiling is declared only where the band arithmetic was measured against a real browser; " + "Firefox was not driven for it). Capture a smaller region or a single element, or use --browser chrome for a tiled capture.");
|
|
3407
|
+
}
|
|
3408
|
+
if (format !== "png")
|
|
3409
|
+
throw tiledCaptureRefusal("format");
|
|
3410
|
+
if (returnBase64)
|
|
3411
|
+
throw tiledCaptureRefusal("base64");
|
|
3412
|
+
return true;
|
|
3413
|
+
}
|
|
3414
|
+
function tiledCaptureRefusal(reason, banded) {
|
|
3415
|
+
const because = banded ? ` This capture needs ${banded.bandCount} bands (${banded.clip.width}x${banded.clip.height} css px is past what one image can encode).` : "";
|
|
3416
|
+
if (reason === "format") {
|
|
3417
|
+
return new SharedToolError("take_screenshot: a tiled capture is PNG-only — bands are stitched by concatenating PNG scanlines, and there is no lossless " + `JPEG equivalent to concatenate.${because} Pass format:"png", or lower scale / narrow the region until one JPEG fits.`);
|
|
3418
|
+
}
|
|
3419
|
+
return new SharedToolError("take_screenshot: 'returnBase64' cannot be combined with a tiled capture — a tiled image runs to hundreds of megabytes and base64 " + `inflates it by a third, into the caller's response.${because} Read the file at the returned path instead, or capture a region small enough for one shot.`);
|
|
3420
|
+
}
|
|
3421
|
+
function pngPixelSize(b, view) {
|
|
3422
|
+
if (b.length < 24)
|
|
3423
|
+
return;
|
|
3424
|
+
if (view.getUint32(0) !== 2303741511 || view.getUint32(4) !== 218765834)
|
|
3425
|
+
return;
|
|
3426
|
+
if (String.fromCharCode(b[12], b[13], b[14], b[15]) !== "IHDR")
|
|
3427
|
+
return;
|
|
3428
|
+
return { width: view.getUint32(16), height: view.getUint32(20) };
|
|
3429
|
+
}
|
|
3430
|
+
var JPEG_SOF = new Set([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]);
|
|
3431
|
+
function jpegPixelSize(b, view) {
|
|
3432
|
+
if (b.length < 4 || b[0] !== 255 || b[1] !== 216)
|
|
3433
|
+
return;
|
|
3434
|
+
let i = 2;
|
|
3435
|
+
while (i + 3 < b.length) {
|
|
3436
|
+
if (b[i] !== 255) {
|
|
3437
|
+
i += 1;
|
|
3438
|
+
continue;
|
|
3439
|
+
}
|
|
3440
|
+
const marker = b[i + 1];
|
|
3441
|
+
if (marker === 255) {
|
|
3442
|
+
i += 1;
|
|
3443
|
+
continue;
|
|
3444
|
+
}
|
|
3445
|
+
if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
|
|
3446
|
+
i += 2;
|
|
3447
|
+
continue;
|
|
3448
|
+
}
|
|
3449
|
+
const segLen = view.getUint16(i + 2);
|
|
3450
|
+
if (JPEG_SOF.has(marker)) {
|
|
3451
|
+
if (i + 9 > b.length)
|
|
3452
|
+
return;
|
|
3453
|
+
return { width: view.getUint16(i + 7), height: view.getUint16(i + 5) };
|
|
3454
|
+
}
|
|
3455
|
+
if (segLen < 2)
|
|
3456
|
+
return;
|
|
3457
|
+
i += 2 + segLen;
|
|
3458
|
+
}
|
|
3459
|
+
return;
|
|
3460
|
+
}
|
|
3461
|
+
function imagePixelSize(bytes) {
|
|
3462
|
+
if (bytes.byteLength < 4)
|
|
3463
|
+
return;
|
|
3464
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
3465
|
+
return pngPixelSize(bytes, view) ?? jpegPixelSize(bytes, view);
|
|
3466
|
+
}
|
|
2633
3467
|
async function takeScreenshot(driver, args = {}) {
|
|
2634
3468
|
if (args.uid !== undefined && args.uid !== "" && args.selector)
|
|
2635
3469
|
throw new SharedToolError("provide exactly one of uid or selector, not both");
|
|
2636
3470
|
const format = args.format ?? "png";
|
|
3471
|
+
const scale = resolveScreenshotScale(args.scale, driver.capabilities);
|
|
3472
|
+
const renderSize = resolveRenderSize(args.renderWidth, args.renderHeight, driver.capabilities);
|
|
3473
|
+
const tile = resolveTileMode(args.tile, driver.capabilities, format, args.returnBase64);
|
|
2637
3474
|
return withPage2(driver, args.target, async (page) => {
|
|
2638
3475
|
const clip = args.uid !== undefined || args.selector ? locatorOf(args) : undefined;
|
|
2639
|
-
const
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
path,
|
|
2648
|
-
bytes: data.byteLength,
|
|
2649
|
-
format: outFormat,
|
|
2650
|
-
target: target3(page.info)
|
|
3476
|
+
const ext = format === "jpeg" ? "jpg" : "png";
|
|
3477
|
+
const path = args.savePath ?? join4(ARTIFACT_DIR, `screenshot-${page.info.id.slice(0, 8)}-${stamp()}.${ext}`);
|
|
3478
|
+
const stitch = async (banded) => {
|
|
3479
|
+
if (format !== "png")
|
|
3480
|
+
throw tiledCaptureRefusal("format", banded);
|
|
3481
|
+
if (args.returnBase64)
|
|
3482
|
+
throw tiledCaptureRefusal("base64", banded);
|
|
3483
|
+
return stitchPngBandsToFile(banded.bands, path);
|
|
2651
3484
|
};
|
|
2652
|
-
|
|
2653
|
-
|
|
3485
|
+
const shot = await page.screenshot({
|
|
3486
|
+
format,
|
|
3487
|
+
quality: args.quality,
|
|
3488
|
+
fullPage: args.fullPage,
|
|
3489
|
+
scale,
|
|
3490
|
+
...clip ? { clip } : {},
|
|
3491
|
+
...renderSize ? { renderWidth: renderSize.width, renderHeight: renderSize.height } : {},
|
|
3492
|
+
...tile !== undefined ? { tile } : {}
|
|
3493
|
+
}, stitch);
|
|
3494
|
+
const result = isTiledCapture(shot) ? {
|
|
3495
|
+
path,
|
|
3496
|
+
bytes: shot.consumed.bytes,
|
|
3497
|
+
format: "png",
|
|
3498
|
+
target: target3(page.info),
|
|
3499
|
+
scale,
|
|
3500
|
+
width: shot.consumed.width,
|
|
3501
|
+
height: shot.consumed.height,
|
|
3502
|
+
tiled: true,
|
|
3503
|
+
bands: shot.consumed.bands
|
|
3504
|
+
} : await writeOneShot(shot.data, shot.format, path, target3(page.info), scale, args.returnBase64);
|
|
3505
|
+
if (renderSize && shot.renderSize) {
|
|
3506
|
+
result.renderSize = shot.renderSize;
|
|
3507
|
+
result.renderRestored = shot.renderRestored ?? false;
|
|
3508
|
+
if (shot.renderRestoreError !== undefined)
|
|
3509
|
+
result.renderRestoreError = shot.renderRestoreError;
|
|
3510
|
+
}
|
|
2654
3511
|
return result;
|
|
2655
3512
|
});
|
|
2656
3513
|
}
|
|
3514
|
+
async function writeOneShot(data, outFormat, path, target, scale, returnBase64) {
|
|
3515
|
+
if (data.byteLength === 0)
|
|
3516
|
+
throw new SharedToolError("captureScreenshot returned empty data");
|
|
3517
|
+
await mkdir5(dirname2(path), { recursive: true });
|
|
3518
|
+
await writeFile4(path, Buffer.from(data));
|
|
3519
|
+
const size = imagePixelSize(data);
|
|
3520
|
+
const result = { path, bytes: data.byteLength, format: outFormat, target, scale };
|
|
3521
|
+
if (size) {
|
|
3522
|
+
result.width = size.width;
|
|
3523
|
+
result.height = size.height;
|
|
3524
|
+
}
|
|
3525
|
+
if (returnBase64)
|
|
3526
|
+
result.base64 = Buffer.from(data).toString("base64");
|
|
3527
|
+
return result;
|
|
3528
|
+
}
|
|
2657
3529
|
async function emulate(driver, args = {}) {
|
|
2658
3530
|
return withPage2(driver, args.target, async (page) => {
|
|
2659
3531
|
if (args.clearOverrides) {
|
|
@@ -2824,10 +3696,10 @@ var LEASE_TOOLS = {
|
|
|
2824
3696
|
};
|
|
2825
3697
|
|
|
2826
3698
|
// src/tools/console.ts
|
|
2827
|
-
import { readFile as
|
|
3699
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
2828
3700
|
|
|
2829
3701
|
// src/tools/recorder.ts
|
|
2830
|
-
import { mkdir as
|
|
3702
|
+
import { mkdir as mkdir6, appendFile, writeFile as writeFile5, copyFile } from "node:fs/promises";
|
|
2831
3703
|
var ARTIFACT_DIR2 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2832
3704
|
function recFile(targetId) {
|
|
2833
3705
|
return `${ARTIFACT_DIR2}/rec-${targetId}.jsonl`;
|
|
@@ -2838,6 +3710,31 @@ function captureFile(targetId, captureId) {
|
|
|
2838
3710
|
function delay2(ms) {
|
|
2839
3711
|
return new Promise((r) => setTimeout(r, ms));
|
|
2840
3712
|
}
|
|
3713
|
+
async function resolveRecorderTarget(target, opts) {
|
|
3714
|
+
if (!isWorkerSelector(target))
|
|
3715
|
+
return resolveTarget(target);
|
|
3716
|
+
if (opts.capture)
|
|
3717
|
+
return resolveWorkerSelector(target, { wake: opts.wake ?? true });
|
|
3718
|
+
const needle = workerNeedle(target);
|
|
3719
|
+
if (needle === "")
|
|
3720
|
+
throw new CdpError(WORKER_EMPTY_NEEDLE_MESSAGE);
|
|
3721
|
+
const { matches, liveWorkers } = resolveWorkerTargets(await listTargets(), needle);
|
|
3722
|
+
if (matches.length > 1)
|
|
3723
|
+
throw new CdpError(workerAmbiguityMessage(needle, matches));
|
|
3724
|
+
if (matches.length === 1)
|
|
3725
|
+
return matches[0];
|
|
3726
|
+
throw new CdpError(workerBufferReadMissMessage(needle, liveWorkers));
|
|
3727
|
+
}
|
|
3728
|
+
function assertWakeApplies(target, wake, capture) {
|
|
3729
|
+
if (wake === undefined)
|
|
3730
|
+
return;
|
|
3731
|
+
if (typeof wake !== "boolean")
|
|
3732
|
+
throw new CdpError("'wake' must be a boolean");
|
|
3733
|
+
if (!isWorkerSelector(target))
|
|
3734
|
+
throw new CdpError(workerWakeMisuseMessage("not-a-worker-selector"));
|
|
3735
|
+
if (!capture)
|
|
3736
|
+
throw new CdpError(workerWakeMisuseMessage("read-only-call"));
|
|
3737
|
+
}
|
|
2841
3738
|
var NETWORK_EVENTS = [
|
|
2842
3739
|
"Network.requestWillBeSent",
|
|
2843
3740
|
"Network.responseReceived",
|
|
@@ -2848,11 +3745,11 @@ var CONSOLE_EVENTS = ["Runtime.consoleAPICalled", "Runtime.exceptionThrown", "Lo
|
|
|
2848
3745
|
async function startRecorder(target, opts = {}) {
|
|
2849
3746
|
const network = opts.network ?? true;
|
|
2850
3747
|
const consoleCap = opts.console ?? true;
|
|
2851
|
-
await
|
|
3748
|
+
await mkdir6(ARTIFACT_DIR2, { recursive: true });
|
|
2852
3749
|
const { conn, target: resolved } = await openPage(target, { timeoutMs: opts.timeoutMs });
|
|
2853
3750
|
const file = opts.file ?? recFile(resolved.id);
|
|
2854
3751
|
if (opts.truncate) {
|
|
2855
|
-
await
|
|
3752
|
+
await writeFile5(file, "");
|
|
2856
3753
|
}
|
|
2857
3754
|
let chain = Promise.resolve();
|
|
2858
3755
|
let dropped = 0;
|
|
@@ -2903,7 +3800,8 @@ async function startRecorder(target, opts = {}) {
|
|
|
2903
3800
|
};
|
|
2904
3801
|
}
|
|
2905
3802
|
async function captureWindow(target, durationMs, opts = {}) {
|
|
2906
|
-
const resolved = await
|
|
3803
|
+
const resolved = await resolveRecorderTarget(target, { capture: true, wake: opts.wake });
|
|
3804
|
+
const isWorker = isWorkerTargetType(resolved.type);
|
|
2907
3805
|
const captureId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2908
3806
|
const file = captureFile(resolved.id, captureId);
|
|
2909
3807
|
const rec = await startRecorder(resolved.id, {
|
|
@@ -2913,13 +3811,16 @@ async function captureWindow(target, durationMs, opts = {}) {
|
|
|
2913
3811
|
file,
|
|
2914
3812
|
timeoutMs: opts.timeoutMs
|
|
2915
3813
|
});
|
|
2916
|
-
|
|
2917
|
-
|
|
3814
|
+
if (!isWorker) {
|
|
3815
|
+
await rec.conn.send("Page.enable");
|
|
3816
|
+
await rec.conn.send("Page.reload", { ignoreCache: false });
|
|
3817
|
+
}
|
|
2918
3818
|
await delay2(durationMs);
|
|
2919
3819
|
return {
|
|
2920
3820
|
file: rec.file,
|
|
2921
3821
|
conn: rec.conn,
|
|
2922
3822
|
resolved: { id: rec.target.id, url: rec.target.url, title: rec.target.title },
|
|
3823
|
+
mode: isWorker ? "listen" : "reload",
|
|
2923
3824
|
droppedWrites: rec.droppedWrites,
|
|
2924
3825
|
flush: () => rec.flush(),
|
|
2925
3826
|
async stop() {
|
|
@@ -2994,7 +3895,7 @@ function lineToEntry(line, index) {
|
|
|
2994
3895
|
async function readConsoleEntries(file) {
|
|
2995
3896
|
let raw;
|
|
2996
3897
|
try {
|
|
2997
|
-
raw = await
|
|
3898
|
+
raw = await readFile4(file, "utf8");
|
|
2998
3899
|
} catch {
|
|
2999
3900
|
return [];
|
|
3000
3901
|
}
|
|
@@ -3019,8 +3920,9 @@ async function readConsoleEntries(file) {
|
|
|
3019
3920
|
return out;
|
|
3020
3921
|
}
|
|
3021
3922
|
async function listConsoleMessages(args = {}) {
|
|
3923
|
+
assertWakeApplies(args.target, args.wake, args.reload === true);
|
|
3022
3924
|
if (args.reload) {
|
|
3023
|
-
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS);
|
|
3925
|
+
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS, { wake: args.wake });
|
|
3024
3926
|
await cap.stop();
|
|
3025
3927
|
const messages2 = await readConsoleEntries(cap.file);
|
|
3026
3928
|
return {
|
|
@@ -3030,7 +3932,7 @@ async function listConsoleMessages(args = {}) {
|
|
|
3030
3932
|
droppedWrites: cap.droppedWrites()
|
|
3031
3933
|
};
|
|
3032
3934
|
}
|
|
3033
|
-
const target = await
|
|
3935
|
+
const target = await resolveRecorderTarget(args.target, { capture: false });
|
|
3034
3936
|
const messages = await readConsoleEntries(recFile(target.id));
|
|
3035
3937
|
return {
|
|
3036
3938
|
target: { id: target.id, url: target.url, title: target.title },
|
|
@@ -3039,7 +3941,7 @@ async function listConsoleMessages(args = {}) {
|
|
|
3039
3941
|
};
|
|
3040
3942
|
}
|
|
3041
3943
|
async function getConsoleMessage(args = {}) {
|
|
3042
|
-
const target = await
|
|
3944
|
+
const target = await resolveRecorderTarget(args.target, { capture: false });
|
|
3043
3945
|
const messages = await readConsoleEntries(recFile(target.id));
|
|
3044
3946
|
const index = args.index ?? 0;
|
|
3045
3947
|
const entry = messages[index];
|
|
@@ -3050,7 +3952,7 @@ async function getConsoleMessage(args = {}) {
|
|
|
3050
3952
|
}
|
|
3051
3953
|
|
|
3052
3954
|
// src/tools/network.ts
|
|
3053
|
-
import { readFile as
|
|
3955
|
+
import { readFile as readFile5 } from "node:fs/promises";
|
|
3054
3956
|
var DEFAULT_CAPTURE_MS2 = 2500;
|
|
3055
3957
|
function asHeaders(v) {
|
|
3056
3958
|
if (!v || typeof v !== "object")
|
|
@@ -3117,7 +4019,7 @@ function correlate(lines) {
|
|
|
3117
4019
|
async function readNetworkRequests(file) {
|
|
3118
4020
|
let raw;
|
|
3119
4021
|
try {
|
|
3120
|
-
raw = await
|
|
4022
|
+
raw = await readFile5(file, "utf8");
|
|
3121
4023
|
} catch {
|
|
3122
4024
|
return [];
|
|
3123
4025
|
}
|
|
@@ -3137,14 +4039,15 @@ async function listNetworkRequests(args = {}) {
|
|
|
3137
4039
|
let file;
|
|
3138
4040
|
let resolved;
|
|
3139
4041
|
let droppedWrites;
|
|
4042
|
+
assertWakeApplies(args.target, args.wake, args.reload === true);
|
|
3140
4043
|
if (args.reload) {
|
|
3141
|
-
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2);
|
|
4044
|
+
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2, { wake: args.wake });
|
|
3142
4045
|
await cap.stop();
|
|
3143
4046
|
file = cap.file;
|
|
3144
4047
|
resolved = cap.resolved;
|
|
3145
4048
|
droppedWrites = cap.droppedWrites();
|
|
3146
4049
|
} else {
|
|
3147
|
-
const target = await
|
|
4050
|
+
const target = await resolveRecorderTarget(args.target, { capture: false });
|
|
3148
4051
|
file = recFile(target.id);
|
|
3149
4052
|
resolved = { id: target.id, url: target.url, title: target.title };
|
|
3150
4053
|
}
|
|
@@ -3165,8 +4068,9 @@ async function getNetworkRequest(args = {}) {
|
|
|
3165
4068
|
return requests.find((r) => r.url.includes(args.url));
|
|
3166
4069
|
};
|
|
3167
4070
|
const describeSelector = () => args.requestId ? `requestId ${args.requestId}` : `url ~ '${args.url}'`;
|
|
4071
|
+
assertWakeApplies(args.target, args.wake, Boolean(args.includeBody && args.url));
|
|
3168
4072
|
if (args.includeBody && args.url) {
|
|
3169
|
-
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2);
|
|
4073
|
+
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2, { wake: args.wake });
|
|
3170
4074
|
try {
|
|
3171
4075
|
await cap.flush();
|
|
3172
4076
|
const match2 = (await readNetworkRequests(cap.file)).find((r) => r.url.includes(args.url));
|
|
@@ -3183,7 +4087,7 @@ async function getNetworkRequest(args = {}) {
|
|
|
3183
4087
|
await cap.stop();
|
|
3184
4088
|
}
|
|
3185
4089
|
}
|
|
3186
|
-
const target = await
|
|
4090
|
+
const target = await resolveRecorderTarget(args.target, { capture: false });
|
|
3187
4091
|
const match = pick(await readNetworkRequests(recFile(target.id)));
|
|
3188
4092
|
if (!match) {
|
|
3189
4093
|
throw new CdpError(`no network request matched ${describeSelector()}; run list_network_requests with reload:true first` + (args.includeBody ? `, then fetch the body with the url selector (reload re-mints requestIds)` : ``));
|
|
@@ -3199,8 +4103,8 @@ async function getNetworkRequest(args = {}) {
|
|
|
3199
4103
|
}
|
|
3200
4104
|
|
|
3201
4105
|
// src/tools/performance.ts
|
|
3202
|
-
import { mkdir as
|
|
3203
|
-
import { join as
|
|
4106
|
+
import { mkdir as mkdir7, readFile as readFile6, writeFile as writeFile6, stat, unlink as unlink4 } from "node:fs/promises";
|
|
4107
|
+
import { join as join5 } from "node:path";
|
|
3204
4108
|
var ARTIFACT_DIR3 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
3205
4109
|
var STATE_DIR2 = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
3206
4110
|
var DEFAULT_CATEGORIES = [
|
|
@@ -3218,7 +4122,7 @@ function shortId(id) {
|
|
|
3218
4122
|
return id.slice(0, 8);
|
|
3219
4123
|
}
|
|
3220
4124
|
function stateFilePath() {
|
|
3221
|
-
return
|
|
4125
|
+
return join5(STATE_DIR2, "trace-state.json");
|
|
3222
4126
|
}
|
|
3223
4127
|
function bufferTraceData(conn, sink) {
|
|
3224
4128
|
return conn.on("Tracing.dataCollected", (params) => {
|
|
@@ -3240,10 +4144,10 @@ async function endTrace(conn, sink, timeoutMs = 30000) {
|
|
|
3240
4144
|
return sink;
|
|
3241
4145
|
}
|
|
3242
4146
|
async function writeTrace(events, targetId) {
|
|
3243
|
-
await
|
|
3244
|
-
const path =
|
|
4147
|
+
await mkdir7(ARTIFACT_DIR3, { recursive: true });
|
|
4148
|
+
const path = join5(ARTIFACT_DIR3, `trace-${shortId(targetId)}-${stamp2()}.json`);
|
|
3245
4149
|
const body = JSON.stringify({ traceEvents: events, metadata: { source: "cdp-toolkit" } });
|
|
3246
|
-
await
|
|
4150
|
+
await writeFile6(path, body, "utf8");
|
|
3247
4151
|
const { size } = await stat(path);
|
|
3248
4152
|
return { path, bytes: size };
|
|
3249
4153
|
}
|
|
@@ -3263,7 +4167,7 @@ async function performanceStartTrace(args = {}) {
|
|
|
3263
4167
|
throw err;
|
|
3264
4168
|
}
|
|
3265
4169
|
liveTraces.set(target.id, { conn, target, events, startedAt: Date.now() });
|
|
3266
|
-
await
|
|
4170
|
+
await mkdir7(STATE_DIR2, { recursive: true });
|
|
3267
4171
|
const state = {
|
|
3268
4172
|
targetId: target.id,
|
|
3269
4173
|
url: target.url,
|
|
@@ -3271,7 +4175,7 @@ async function performanceStartTrace(args = {}) {
|
|
|
3271
4175
|
pid: process.pid,
|
|
3272
4176
|
categories
|
|
3273
4177
|
};
|
|
3274
|
-
await
|
|
4178
|
+
await writeFile6(stateFilePath(), JSON.stringify(state), "utf8");
|
|
3275
4179
|
return {
|
|
3276
4180
|
target: { id: target.id, url: target.url, title: target.title },
|
|
3277
4181
|
categories,
|
|
@@ -3284,7 +4188,7 @@ async function performanceStopTrace(args = {}) {
|
|
|
3284
4188
|
if (!entry) {
|
|
3285
4189
|
let hint = "";
|
|
3286
4190
|
try {
|
|
3287
|
-
const raw = await
|
|
4191
|
+
const raw = await readFile6(stateFilePath(), "utf8");
|
|
3288
4192
|
const state = JSON.parse(raw);
|
|
3289
4193
|
hint = state.pid === process.pid ? ` (state file lists target ${shortId(state.targetId)} but its live connection is gone)` : ` (trace was started by a DIFFERENT process pid=${state.pid}; live recordings cannot cross process boundaries)`;
|
|
3290
4194
|
} catch {}
|
|
@@ -3296,7 +4200,7 @@ async function performanceStopTrace(args = {}) {
|
|
|
3296
4200
|
} finally {
|
|
3297
4201
|
conn.close();
|
|
3298
4202
|
liveTraces.delete(target.id);
|
|
3299
|
-
await
|
|
4203
|
+
await unlink4(stateFilePath()).catch(() => {});
|
|
3300
4204
|
}
|
|
3301
4205
|
const written = await writeTrace(events, target.id);
|
|
3302
4206
|
const metrics = analyzeEvents(events);
|
|
@@ -3414,7 +4318,7 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
3414
4318
|
}
|
|
3415
4319
|
let raw;
|
|
3416
4320
|
try {
|
|
3417
|
-
raw = await
|
|
4321
|
+
raw = await readFile6(args.tracePath, "utf8");
|
|
3418
4322
|
} catch (err) {
|
|
3419
4323
|
throw new CdpError(`cannot read trace file '${args.tracePath}': ${err.message}`);
|
|
3420
4324
|
}
|
|
@@ -3432,8 +4336,8 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
3432
4336
|
|
|
3433
4337
|
// src/tools/screencast.ts
|
|
3434
4338
|
import { spawn } from "node:child_process";
|
|
3435
|
-
import { mkdir as
|
|
3436
|
-
import { join as
|
|
4339
|
+
import { mkdir as mkdir8, rm as rm2, stat as stat2, writeFile as writeFile7 } from "node:fs/promises";
|
|
4340
|
+
import { join as join6 } from "node:path";
|
|
3437
4341
|
var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
3438
4342
|
var FFMPEG_BIN = process.env.CDP_FFMPEG ?? "ffmpeg";
|
|
3439
4343
|
function frameDurationsMs(ledger, stopWallclockMs) {
|
|
@@ -3489,7 +4393,7 @@ function pngSize(buf) {
|
|
|
3489
4393
|
return;
|
|
3490
4394
|
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
3491
4395
|
}
|
|
3492
|
-
var
|
|
4396
|
+
var JPEG_SOF2 = new Set([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]);
|
|
3493
4397
|
function jpegSize(buf) {
|
|
3494
4398
|
if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216)
|
|
3495
4399
|
return;
|
|
@@ -3511,7 +4415,7 @@ function jpegSize(buf) {
|
|
|
3511
4415
|
if (i + 3 >= buf.length)
|
|
3512
4416
|
return;
|
|
3513
4417
|
const segLen = buf.readUInt16BE(i + 2);
|
|
3514
|
-
if (
|
|
4418
|
+
if (JPEG_SOF2.has(marker)) {
|
|
3515
4419
|
if (i + 9 > buf.length)
|
|
3516
4420
|
return;
|
|
3517
4421
|
return { width: buf.readUInt16BE(i + 7), height: buf.readUInt16BE(i + 5) };
|
|
@@ -3654,9 +4558,9 @@ async function startScreenRecording(args = {}) {
|
|
|
3654
4558
|
conn.close();
|
|
3655
4559
|
throw err;
|
|
3656
4560
|
}
|
|
3657
|
-
const spoolDir =
|
|
4561
|
+
const spoolDir = join6(ARTIFACT_DIR4, `screencast-${shortId2(target.id)}-${stamp3()}`);
|
|
3658
4562
|
try {
|
|
3659
|
-
await
|
|
4563
|
+
await mkdir8(spoolDir, { recursive: true });
|
|
3660
4564
|
} catch (err) {
|
|
3661
4565
|
conn.close();
|
|
3662
4566
|
throw new CdpError(`cannot create the screencast spool directory '${spoolDir}': ${err.message}`);
|
|
@@ -3697,7 +4601,7 @@ async function startScreenRecording(args = {}) {
|
|
|
3697
4601
|
seq += 1;
|
|
3698
4602
|
const file = `frame-${String(seq).padStart(6, "0")}.${ext}`;
|
|
3699
4603
|
rec.ledger.push({ file, timestamp, width: size.width, height: size.height });
|
|
3700
|
-
rec.writes = rec.writes.then(() =>
|
|
4604
|
+
rec.writes = rec.writes.then(() => writeFile7(join6(spoolDir, file), buf)).catch(() => {
|
|
3701
4605
|
rec.dropped += 1;
|
|
3702
4606
|
rec.unwritten.add(file);
|
|
3703
4607
|
});
|
|
@@ -3719,7 +4623,7 @@ async function startScreenRecording(args = {}) {
|
|
|
3719
4623
|
} catch (err) {
|
|
3720
4624
|
rec.off();
|
|
3721
4625
|
conn.close();
|
|
3722
|
-
await
|
|
4626
|
+
await rm2(spoolDir, { recursive: true, force: true }).catch(() => {});
|
|
3723
4627
|
throw err;
|
|
3724
4628
|
}
|
|
3725
4629
|
liveRecordings.set(target.id, rec);
|
|
@@ -3757,14 +4661,14 @@ async function stopScreenRecording(args = {}) {
|
|
|
3757
4661
|
liveRecordings.delete(rec.target.id);
|
|
3758
4662
|
const ledger = rec.unwritten.size ? rec.ledger.filter((entry) => !rec.unwritten.has(entry.file)) : rec.ledger;
|
|
3759
4663
|
if (!ledger.length) {
|
|
3760
|
-
await
|
|
4664
|
+
await rm2(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
|
|
3761
4665
|
throw new CdpError(`screen recording captured 0 frames for target ${shortId2(rec.target.id)}. Chrome emits a screencast frame only on repaint: a fully backgrounded or occluded tab may never paint. Retry with bringToFront:true, or drive the page while recording.`);
|
|
3762
4666
|
}
|
|
3763
|
-
await
|
|
3764
|
-
const outPath = args.savePath ??
|
|
4667
|
+
await mkdir8(ARTIFACT_DIR4, { recursive: true });
|
|
4668
|
+
const outPath = args.savePath ?? join6(ARTIFACT_DIR4, `screen-recording-${shortId2(rec.target.id)}-${stamp3()}.mp4`);
|
|
3765
4669
|
const frames = coalesceToConcatGrid(ledger, stoppedAt);
|
|
3766
|
-
const manifestPath =
|
|
3767
|
-
await
|
|
4670
|
+
const manifestPath = join6(rec.spoolDir, "frames.ffconcat");
|
|
4671
|
+
await writeFile7(manifestPath, buildConcatManifest(frames), "utf8");
|
|
3768
4672
|
const durationMs = frames.reduce((total, f) => total + f.durationMs, 0);
|
|
3769
4673
|
const { width, height } = evenDimensions(ledger[0].width, ledger[0].height);
|
|
3770
4674
|
let res;
|
|
@@ -3778,7 +4682,7 @@ async function stopScreenRecording(args = {}) {
|
|
|
3778
4682
|
ffmpeg said: ${res.stderr.trim().slice(0, 800)}`);
|
|
3779
4683
|
}
|
|
3780
4684
|
const { size } = await stat2(outPath);
|
|
3781
|
-
await
|
|
4685
|
+
await rm2(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
|
|
3782
4686
|
return {
|
|
3783
4687
|
path: outPath,
|
|
3784
4688
|
bytes: size,
|
|
@@ -3795,8 +4699,8 @@ ffmpeg said: ${res.stderr.trim().slice(0, 800)}`);
|
|
|
3795
4699
|
}
|
|
3796
4700
|
|
|
3797
4701
|
// src/tools/heap.ts
|
|
3798
|
-
import { mkdir as
|
|
3799
|
-
import { join as
|
|
4702
|
+
import { mkdir as mkdir9, writeFile as writeFile8 } from "node:fs/promises";
|
|
4703
|
+
import { join as join7 } from "node:path";
|
|
3800
4704
|
var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
3801
4705
|
function stamp4(targetId) {
|
|
3802
4706
|
const short = targetId.slice(0, 8);
|
|
@@ -3821,9 +4725,9 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
3821
4725
|
if (snapshot.length === 0) {
|
|
3822
4726
|
throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
|
|
3823
4727
|
}
|
|
3824
|
-
await
|
|
3825
|
-
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath :
|
|
3826
|
-
await
|
|
4728
|
+
await mkdir9(ARTIFACT_DIR5, { recursive: true });
|
|
4729
|
+
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join7(ARTIFACT_DIR5, args.savePath) : join7(ARTIFACT_DIR5, `take_heapsnapshot-${stamp4(target.id)}.heapsnapshot`);
|
|
4730
|
+
await writeFile8(path, snapshot, "utf8");
|
|
3827
4731
|
const bytes = Buffer.byteLength(snapshot, "utf8");
|
|
3828
4732
|
return {
|
|
3829
4733
|
path,
|
|
@@ -3836,7 +4740,7 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
3836
4740
|
|
|
3837
4741
|
// src/tools/lighthouse.ts
|
|
3838
4742
|
import { spawn as spawn2 } from "node:child_process";
|
|
3839
|
-
import { mkdir as
|
|
4743
|
+
import { mkdir as mkdir10, readFile as readFile7, stat as stat3 } from "node:fs/promises";
|
|
3840
4744
|
var ARTIFACT_DIR6 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
3841
4745
|
function cdpPort() {
|
|
3842
4746
|
const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
@@ -3897,7 +4801,7 @@ async function lighthouseAudit(args) {
|
|
|
3897
4801
|
const url = args.url.trim();
|
|
3898
4802
|
const timeoutMs = args.timeoutMs ?? 120000;
|
|
3899
4803
|
const port = cdpPort();
|
|
3900
|
-
await
|
|
4804
|
+
await mkdir10(ARTIFACT_DIR6, { recursive: true });
|
|
3901
4805
|
const outPath = args.savePath ?? `${ARTIFACT_DIR6}/lighthouse-${shortHost(url)}-${stamp5()}.json`;
|
|
3902
4806
|
const formFactor = args.formFactor ?? "desktop";
|
|
3903
4807
|
const lhArgs = [
|
|
@@ -3926,7 +4830,7 @@ async function lighthouseAudit(args) {
|
|
|
3926
4830
|
}
|
|
3927
4831
|
let raw;
|
|
3928
4832
|
try {
|
|
3929
|
-
raw = await
|
|
4833
|
+
raw = await readFile7(outPath, "utf8");
|
|
3930
4834
|
} catch {
|
|
3931
4835
|
raw = undefined;
|
|
3932
4836
|
}
|
|
@@ -4013,14 +4917,14 @@ async function dispatchMouse(args) {
|
|
|
4013
4917
|
}
|
|
4014
4918
|
|
|
4015
4919
|
// src/tools/downloads.ts
|
|
4016
|
-
import { readdir as readdir3, rename, stat as stat4 } from "node:fs/promises";
|
|
4017
|
-
import { basename, extname, join as
|
|
4920
|
+
import { readdir as readdir3, rename as rename2, stat as stat4 } from "node:fs/promises";
|
|
4921
|
+
import { basename, extname, join as join9 } from "node:path";
|
|
4018
4922
|
|
|
4019
4923
|
// src/tools/browser-session.ts
|
|
4020
|
-
import { mkdir as
|
|
4021
|
-
import { join as
|
|
4924
|
+
import { mkdir as mkdir11 } from "node:fs/promises";
|
|
4925
|
+
import { join as join8 } from "node:path";
|
|
4022
4926
|
var ARTIFACT_DIR7 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
4023
|
-
var DOWNLOADS_DIR =
|
|
4927
|
+
var DOWNLOADS_DIR = join8(ARTIFACT_DIR7, "downloads");
|
|
4024
4928
|
var DOWNLOAD_BUFFER_LIMIT = 50;
|
|
4025
4929
|
var session;
|
|
4026
4930
|
function isDisconnect(e) {
|
|
@@ -4092,7 +4996,7 @@ async function browserSend(method, params = {}) {
|
|
|
4092
4996
|
}
|
|
4093
4997
|
}
|
|
4094
4998
|
async function armDownloadsAt(path) {
|
|
4095
|
-
await
|
|
4999
|
+
await mkdir11(path, { recursive: true });
|
|
4096
5000
|
const s = await getBrowserSession();
|
|
4097
5001
|
await s.conn.send("Browser.setDownloadBehavior", { behavior: "allowAndName", downloadPath: path, eventsEnabled: true });
|
|
4098
5002
|
s.armedPath = path;
|
|
@@ -4182,8 +5086,8 @@ async function waitForDownload(args = {}) {
|
|
|
4182
5086
|
const taken = new Set(await readdir3(DOWNLOADS_DIR).catch(() => []));
|
|
4183
5087
|
taken.delete(basename(rec.filePath));
|
|
4184
5088
|
const finalName = nextFreeName(suggestedFilename, taken);
|
|
4185
|
-
const path =
|
|
4186
|
-
await
|
|
5089
|
+
const path = join9(DOWNLOADS_DIR, finalName);
|
|
5090
|
+
await rename2(rec.filePath, path);
|
|
4187
5091
|
const bytes = (await stat4(path)).size;
|
|
4188
5092
|
return { path, suggestedFilename, bytes, ...rec.url ? { url: rec.url } : {}, target };
|
|
4189
5093
|
}
|