@saptools/cf-inspector 0.6.2 → 0.7.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 +51 -14
- package/dist/cli.js +1371 -480
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +70 -1
- package/dist/index.js +579 -14
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -913,6 +913,56 @@ async function fetchInspectorVersion(host, port, timeoutMs) {
|
|
|
913
913
|
}
|
|
914
914
|
return { browser, protocolVersion };
|
|
915
915
|
}
|
|
916
|
+
function startInspectorKeepalive(host, port, options = {}) {
|
|
917
|
+
const intervalMs = options.intervalMs ?? 1e4;
|
|
918
|
+
const probeTimeoutMs = options.probeTimeoutMs ?? 2e3;
|
|
919
|
+
const failureThreshold = options.failureThreshold ?? 3;
|
|
920
|
+
const probe = options.probe ?? (async () => await fetchInspectorVersion(host, port, probeTimeoutMs));
|
|
921
|
+
let cancelled = false;
|
|
922
|
+
let consecutiveFailures = 0;
|
|
923
|
+
let timer;
|
|
924
|
+
let rejectFailure;
|
|
925
|
+
const failure = new Promise((_resolve, reject) => {
|
|
926
|
+
rejectFailure = reject;
|
|
927
|
+
});
|
|
928
|
+
const schedule = () => {
|
|
929
|
+
if (cancelled) {
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
timer = setTimeout(() => {
|
|
933
|
+
void runProbe();
|
|
934
|
+
}, intervalMs);
|
|
935
|
+
};
|
|
936
|
+
const runProbe = async () => {
|
|
937
|
+
try {
|
|
938
|
+
await probe();
|
|
939
|
+
consecutiveFailures = 0;
|
|
940
|
+
} catch (error) {
|
|
941
|
+
consecutiveFailures += 1;
|
|
942
|
+
if (consecutiveFailures >= failureThreshold) {
|
|
943
|
+
cancelled = true;
|
|
944
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
945
|
+
rejectFailure?.(new CfInspectorError(
|
|
946
|
+
"INSPECTOR_CONNECTION_FAILED",
|
|
947
|
+
`Inspector tunnel ${host}:${port.toString()} failed ${failureThreshold.toString()} consecutive keepalive probes and is no longer round-tripping. Retry the command after restarting or investigating the owning tunnel session.`,
|
|
948
|
+
detail
|
|
949
|
+
));
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
schedule();
|
|
954
|
+
};
|
|
955
|
+
schedule();
|
|
956
|
+
return {
|
|
957
|
+
failure,
|
|
958
|
+
cancel: () => {
|
|
959
|
+
cancelled = true;
|
|
960
|
+
if (timer !== void 0) {
|
|
961
|
+
clearTimeout(timer);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
}
|
|
916
966
|
|
|
917
967
|
// src/inspector/pause.ts
|
|
918
968
|
init_types();
|
|
@@ -1035,10 +1085,16 @@ async function waitForLivePause(session, options, deadlineMs) {
|
|
|
1035
1085
|
return toPauseEvent(params, receivedAtMs ?? performance2.now(), session.scripts);
|
|
1036
1086
|
}
|
|
1037
1087
|
|
|
1088
|
+
// src/inspector/fanout.ts
|
|
1089
|
+
init_types();
|
|
1090
|
+
import { performance as performance3 } from "perf_hooks";
|
|
1091
|
+
|
|
1038
1092
|
// src/inspector/runtime.ts
|
|
1039
1093
|
init_types();
|
|
1040
1094
|
async function resume(session) {
|
|
1041
1095
|
await session.client.send("Debugger.resume");
|
|
1096
|
+
session.debuggerState.paused = false;
|
|
1097
|
+
delete session.debuggerState.currentPause;
|
|
1042
1098
|
}
|
|
1043
1099
|
async function setPauseOnExceptions(session, state) {
|
|
1044
1100
|
await session.client.send("Debugger.setPauseOnExceptions", { state });
|
|
@@ -1154,8 +1210,264 @@ async function releaseObjectGroup(session, objectGroup) {
|
|
|
1154
1210
|
await session.client.send("Runtime.releaseObjectGroup", { objectGroup });
|
|
1155
1211
|
}
|
|
1156
1212
|
|
|
1213
|
+
// src/inspector/fanout.ts
|
|
1214
|
+
var DEFAULT_CLEANUP_TIMEOUT_MS = 2e3;
|
|
1215
|
+
var BreakpointFanout = class {
|
|
1216
|
+
records = /* @__PURE__ */ new Map();
|
|
1217
|
+
setupErrors = [];
|
|
1218
|
+
detach;
|
|
1219
|
+
detachRemoved;
|
|
1220
|
+
detachError;
|
|
1221
|
+
activeRace;
|
|
1222
|
+
pauseReasons = [];
|
|
1223
|
+
constructor(group, setupSession, pauseReasons = []) {
|
|
1224
|
+
this.pauseReasons = pauseReasons;
|
|
1225
|
+
this.detach = group.onSession((session) => {
|
|
1226
|
+
const record = { session, handles: [], setup: Promise.resolve() };
|
|
1227
|
+
this.records.set(session, record);
|
|
1228
|
+
record.setup = setupSession(session, (handle) => {
|
|
1229
|
+
this.trackHandle(session, handle);
|
|
1230
|
+
}).then((result) => {
|
|
1231
|
+
for (const handle of result.handles) {
|
|
1232
|
+
this.trackHandle(session, handle);
|
|
1233
|
+
}
|
|
1234
|
+
});
|
|
1235
|
+
const setup = record.setup;
|
|
1236
|
+
setup.catch((error) => {
|
|
1237
|
+
for (const reject of this.setupErrors) {
|
|
1238
|
+
reject(error);
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
this.activeRace?.add(record);
|
|
1242
|
+
});
|
|
1243
|
+
this.detachRemoved = group.onSessionRemoved((session) => {
|
|
1244
|
+
this.records.delete(session);
|
|
1245
|
+
this.activeRace?.remove(session);
|
|
1246
|
+
});
|
|
1247
|
+
this.detachError = group.onError((error) => {
|
|
1248
|
+
for (const reject of this.setupErrors) {
|
|
1249
|
+
reject(error);
|
|
1250
|
+
}
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
async ready() {
|
|
1254
|
+
await Promise.all([...this.records.values()].map((record) => record.setup));
|
|
1255
|
+
}
|
|
1256
|
+
trackHandle(session, handle) {
|
|
1257
|
+
const record = this.records.get(session);
|
|
1258
|
+
if (record !== void 0 && !record.handles.some((candidate) => candidate.breakpointId === handle.breakpointId)) {
|
|
1259
|
+
record.handles.push(handle);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
availableOutcomes() {
|
|
1263
|
+
return [...this.records.values()].map((record) => ({
|
|
1264
|
+
session: record.session,
|
|
1265
|
+
setup: { handles: record.handles }
|
|
1266
|
+
}));
|
|
1267
|
+
}
|
|
1268
|
+
async waitForFirst(timeoutMs, options = {}, signal) {
|
|
1269
|
+
if (this.activeRace !== void 0) {
|
|
1270
|
+
throw new CfInspectorError("INVALID_ARGUMENT", "A fan-out pause race is already active");
|
|
1271
|
+
}
|
|
1272
|
+
const race = new ActivePauseRace(timeoutMs, options, signal);
|
|
1273
|
+
this.pauseReasons = options.pauseReasons ?? [];
|
|
1274
|
+
this.activeRace = race;
|
|
1275
|
+
this.setupErrors.push(race.reject);
|
|
1276
|
+
for (const record of this.records.values()) {
|
|
1277
|
+
race.add(record);
|
|
1278
|
+
}
|
|
1279
|
+
try {
|
|
1280
|
+
const winner = await race.result;
|
|
1281
|
+
await race.stopAndSettle();
|
|
1282
|
+
await this.resumePausedLosers(winner.session);
|
|
1283
|
+
return winner;
|
|
1284
|
+
} finally {
|
|
1285
|
+
this.activeRace = void 0;
|
|
1286
|
+
const index = this.setupErrors.indexOf(race.reject);
|
|
1287
|
+
if (index >= 0) {
|
|
1288
|
+
this.setupErrors.splice(index, 1);
|
|
1289
|
+
}
|
|
1290
|
+
await race.stopAndSettle();
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
async resumePaused(except) {
|
|
1294
|
+
return await this.resumePausedLosers(except, DEFAULT_CLEANUP_TIMEOUT_MS);
|
|
1295
|
+
}
|
|
1296
|
+
async cleanup(timeoutMs = DEFAULT_CLEANUP_TIMEOUT_MS, preservePaused) {
|
|
1297
|
+
this.detach();
|
|
1298
|
+
this.detachRemoved();
|
|
1299
|
+
this.detachError();
|
|
1300
|
+
const deadline = performance3.now() + timeoutMs;
|
|
1301
|
+
await settleWithin(Promise.allSettled([...this.records.values()].map(async (record) => {
|
|
1302
|
+
await record.setup;
|
|
1303
|
+
})), remaining(deadline));
|
|
1304
|
+
const breakpointEntries = [...this.records.values()].flatMap((record) => record.handles.map((handle) => ({
|
|
1305
|
+
session: record.session,
|
|
1306
|
+
breakpointId: handle.breakpointId
|
|
1307
|
+
})));
|
|
1308
|
+
let cleared = 0;
|
|
1309
|
+
const clearWork = Promise.allSettled(breakpointEntries.map(async (entry) => {
|
|
1310
|
+
await removeBreakpoint(entry.session, entry.breakpointId);
|
|
1311
|
+
cleared += 1;
|
|
1312
|
+
}));
|
|
1313
|
+
await settleWithin(clearWork, remaining(deadline));
|
|
1314
|
+
const resumed = await this.resumePausedLosers(preservePaused, remaining(deadline));
|
|
1315
|
+
return { attempted: breakpointEntries.length, cleared, resumed };
|
|
1316
|
+
}
|
|
1317
|
+
async resumePausedLosers(except, timeoutMs = DEFAULT_CLEANUP_TIMEOUT_MS) {
|
|
1318
|
+
let resumed = 0;
|
|
1319
|
+
await settleWithin(Promise.allSettled([...this.records.keys()].map(async (session) => {
|
|
1320
|
+
if (session === except || session.debuggerState.paused !== true || session.client.isClosed || !this.ownsCurrentPause(session)) {
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
await resume(session);
|
|
1324
|
+
session.debuggerState.paused = false;
|
|
1325
|
+
resumed += 1;
|
|
1326
|
+
})), timeoutMs);
|
|
1327
|
+
return resumed;
|
|
1328
|
+
}
|
|
1329
|
+
ownsCurrentPause(session) {
|
|
1330
|
+
const pause = session.debuggerState.currentPause;
|
|
1331
|
+
if (pause === void 0) {
|
|
1332
|
+
return false;
|
|
1333
|
+
}
|
|
1334
|
+
if (this.pauseReasons.includes(pause.reason)) {
|
|
1335
|
+
return true;
|
|
1336
|
+
}
|
|
1337
|
+
const record = this.records.get(session);
|
|
1338
|
+
const breakpointIds = new Set(record?.handles.map((handle) => handle.breakpointId) ?? []);
|
|
1339
|
+
return pause.hitBreakpoints.some((breakpointId) => breakpointIds.has(breakpointId));
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
var ActivePauseRace = class {
|
|
1343
|
+
constructor(timeoutMs, options, signal) {
|
|
1344
|
+
this.options = options;
|
|
1345
|
+
this.externalSignal = signal;
|
|
1346
|
+
this.deadline = performance3.now() + timeoutMs;
|
|
1347
|
+
let resolveResult;
|
|
1348
|
+
let rejectResult;
|
|
1349
|
+
this.result = new Promise((resolve, reject) => {
|
|
1350
|
+
resolveResult = resolve;
|
|
1351
|
+
rejectResult = reject;
|
|
1352
|
+
});
|
|
1353
|
+
this.resolveResult = (winner) => {
|
|
1354
|
+
if (this.settled) {
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
this.settled = true;
|
|
1358
|
+
resolveResult?.(winner);
|
|
1359
|
+
};
|
|
1360
|
+
this.reject = (error) => {
|
|
1361
|
+
if (this.settled) {
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
this.settled = true;
|
|
1365
|
+
rejectResult?.(error);
|
|
1366
|
+
};
|
|
1367
|
+
this.timeout = setTimeout(() => {
|
|
1368
|
+
this.reject(this.terminalTimeoutError ?? new CfInspectorError(
|
|
1369
|
+
"BREAKPOINT_NOT_HIT",
|
|
1370
|
+
`Timed out waiting for a matching pause in any isolate after ${timeoutMs.toString()}ms`
|
|
1371
|
+
));
|
|
1372
|
+
this.controller.abort();
|
|
1373
|
+
}, timeoutMs + 25);
|
|
1374
|
+
if (signal !== void 0) {
|
|
1375
|
+
if (signal.aborted) {
|
|
1376
|
+
this.reject(new CfInspectorError("ABORTED", "Aborted while waiting for an isolate pause"));
|
|
1377
|
+
} else {
|
|
1378
|
+
signal.addEventListener("abort", this.onExternalAbort, { once: true });
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
options;
|
|
1383
|
+
controller = new AbortController();
|
|
1384
|
+
waits = /* @__PURE__ */ new Set();
|
|
1385
|
+
settled = false;
|
|
1386
|
+
deadline;
|
|
1387
|
+
timeout;
|
|
1388
|
+
externalSignal;
|
|
1389
|
+
onExternalAbort = () => {
|
|
1390
|
+
this.reject(new CfInspectorError("ABORTED", "Aborted while waiting for an isolate pause"));
|
|
1391
|
+
this.controller.abort();
|
|
1392
|
+
};
|
|
1393
|
+
resolveResult;
|
|
1394
|
+
terminalTimeoutError;
|
|
1395
|
+
removedSessions = /* @__PURE__ */ new Set();
|
|
1396
|
+
reject;
|
|
1397
|
+
result;
|
|
1398
|
+
add(record) {
|
|
1399
|
+
if (this.settled) {
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
const wait = this.wait(record).finally(() => {
|
|
1403
|
+
this.waits.delete(wait);
|
|
1404
|
+
});
|
|
1405
|
+
this.waits.add(wait);
|
|
1406
|
+
}
|
|
1407
|
+
remove(session) {
|
|
1408
|
+
this.removedSessions.add(session);
|
|
1409
|
+
}
|
|
1410
|
+
async stopAndSettle() {
|
|
1411
|
+
clearTimeout(this.timeout);
|
|
1412
|
+
this.externalSignal?.removeEventListener("abort", this.onExternalAbort);
|
|
1413
|
+
this.controller.abort();
|
|
1414
|
+
await Promise.allSettled([...this.waits]);
|
|
1415
|
+
}
|
|
1416
|
+
async wait(record) {
|
|
1417
|
+
try {
|
|
1418
|
+
await record.setup;
|
|
1419
|
+
const remainingMs = Math.max(1, this.deadline - performance3.now());
|
|
1420
|
+
const pause = await waitForPause(record.session, {
|
|
1421
|
+
...this.options,
|
|
1422
|
+
timeoutMs: remainingMs,
|
|
1423
|
+
breakpointIds: record.handles.map((handle) => handle.breakpointId),
|
|
1424
|
+
signal: this.controller.signal
|
|
1425
|
+
});
|
|
1426
|
+
record.session.debuggerState.paused = true;
|
|
1427
|
+
this.resolveResult({ session: record.session, pause });
|
|
1428
|
+
this.controller.abort();
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
if (this.removedSessions.has(record.session)) {
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
if (error instanceof CfInspectorError && error.code === "UNRELATED_PAUSE_TIMEOUT") {
|
|
1434
|
+
this.terminalTimeoutError = error;
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
if (isExpectedRaceStop(error)) {
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
this.reject(error);
|
|
1441
|
+
this.controller.abort();
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
function isExpectedRaceStop(error) {
|
|
1446
|
+
return error instanceof CfInspectorError && (error.code === "ABORTED" || error.code === "BREAKPOINT_NOT_HIT" || error.code === "UNRELATED_PAUSE_TIMEOUT");
|
|
1447
|
+
}
|
|
1448
|
+
function remaining(deadline) {
|
|
1449
|
+
return Math.max(0, deadline - performance3.now());
|
|
1450
|
+
}
|
|
1451
|
+
async function settleWithin(work, timeoutMs) {
|
|
1452
|
+
let timer;
|
|
1453
|
+
try {
|
|
1454
|
+
return await Promise.race([
|
|
1455
|
+
work,
|
|
1456
|
+
new Promise((resolve) => {
|
|
1457
|
+
timer = setTimeout(() => {
|
|
1458
|
+
resolve(null);
|
|
1459
|
+
}, timeoutMs);
|
|
1460
|
+
})
|
|
1461
|
+
]);
|
|
1462
|
+
} finally {
|
|
1463
|
+
if (timer !== void 0) {
|
|
1464
|
+
clearTimeout(timer);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1157
1469
|
// src/inspector/session.ts
|
|
1158
|
-
import { performance as
|
|
1470
|
+
import { performance as performance4 } from "perf_hooks";
|
|
1159
1471
|
|
|
1160
1472
|
// src/cdp/client.ts
|
|
1161
1473
|
init_types();
|
|
@@ -1518,6 +1830,7 @@ init_types();
|
|
|
1518
1830
|
var DEFAULT_CONNECT_TIMEOUT_MS = 5e3;
|
|
1519
1831
|
var DEFAULT_HOST = "127.0.0.1";
|
|
1520
1832
|
var PAUSE_BUFFER_LIMIT = 32;
|
|
1833
|
+
var WORKER_DISCOVERY_SETTLE_MS = 500;
|
|
1521
1834
|
var NodeWorkerDiscovery = class {
|
|
1522
1835
|
constructor(client) {
|
|
1523
1836
|
this.client = client;
|
|
@@ -1526,12 +1839,18 @@ var NodeWorkerDiscovery = class {
|
|
|
1526
1839
|
const worker = toInspectorWorkerTarget(raw);
|
|
1527
1840
|
if (worker !== void 0) {
|
|
1528
1841
|
this.workers.set(worker.sessionId, worker);
|
|
1842
|
+
for (const listener of this.attachedListeners) {
|
|
1843
|
+
listener(worker);
|
|
1844
|
+
}
|
|
1529
1845
|
}
|
|
1530
1846
|
}),
|
|
1531
1847
|
client.on("NodeWorker.detachedFromWorker", (raw) => {
|
|
1532
1848
|
const sessionId = readField(raw, "sessionId");
|
|
1533
1849
|
if (typeof sessionId === "string") {
|
|
1534
1850
|
this.workers.delete(sessionId);
|
|
1851
|
+
for (const listener of this.detachedListeners) {
|
|
1852
|
+
listener(sessionId);
|
|
1853
|
+
}
|
|
1535
1854
|
}
|
|
1536
1855
|
})
|
|
1537
1856
|
];
|
|
@@ -1541,6 +1860,8 @@ var NodeWorkerDiscovery = class {
|
|
|
1541
1860
|
detachListeners;
|
|
1542
1861
|
supported = false;
|
|
1543
1862
|
disposed = false;
|
|
1863
|
+
attachedListeners = /* @__PURE__ */ new Set();
|
|
1864
|
+
detachedListeners = /* @__PURE__ */ new Set();
|
|
1544
1865
|
async enable() {
|
|
1545
1866
|
try {
|
|
1546
1867
|
await this.client.send("NodeWorker.enable", { waitForDebuggerOnStart: false });
|
|
@@ -1554,6 +1875,45 @@ var NodeWorkerDiscovery = class {
|
|
|
1554
1875
|
list() {
|
|
1555
1876
|
return [...this.workers.values()].sort(compareWorkers);
|
|
1556
1877
|
}
|
|
1878
|
+
onAttached(listener) {
|
|
1879
|
+
this.attachedListeners.add(listener);
|
|
1880
|
+
return () => {
|
|
1881
|
+
this.attachedListeners.delete(listener);
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
onDetached(listener) {
|
|
1885
|
+
this.detachedListeners.add(listener);
|
|
1886
|
+
return () => {
|
|
1887
|
+
this.detachedListeners.delete(listener);
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
async waitFor(predicate, timeoutMs = WORKER_DISCOVERY_SETTLE_MS) {
|
|
1891
|
+
const existing = predicate(this.list());
|
|
1892
|
+
if (existing !== void 0) {
|
|
1893
|
+
return existing;
|
|
1894
|
+
}
|
|
1895
|
+
return await new Promise((resolve) => {
|
|
1896
|
+
let settled = false;
|
|
1897
|
+
const finish = (worker) => {
|
|
1898
|
+
if (settled) {
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
settled = true;
|
|
1902
|
+
clearTimeout(timer);
|
|
1903
|
+
detach();
|
|
1904
|
+
resolve(worker);
|
|
1905
|
+
};
|
|
1906
|
+
const detach = this.onAttached(() => {
|
|
1907
|
+
const worker = predicate(this.list());
|
|
1908
|
+
if (worker !== void 0) {
|
|
1909
|
+
finish(worker);
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1912
|
+
const timer = setTimeout(() => {
|
|
1913
|
+
finish();
|
|
1914
|
+
}, timeoutMs);
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1557
1917
|
async dispose() {
|
|
1558
1918
|
if (this.disposed) {
|
|
1559
1919
|
return;
|
|
@@ -1568,6 +1928,8 @@ var NodeWorkerDiscovery = class {
|
|
|
1568
1928
|
for (const detach of this.detachListeners) {
|
|
1569
1929
|
detach();
|
|
1570
1930
|
}
|
|
1931
|
+
this.attachedListeners.clear();
|
|
1932
|
+
this.detachedListeners.clear();
|
|
1571
1933
|
}
|
|
1572
1934
|
};
|
|
1573
1935
|
function isUnsupportedNodeWorkerDomain(error) {
|
|
@@ -1632,14 +1994,15 @@ async function connectInspector(options) {
|
|
|
1632
1994
|
let workerDiscovery;
|
|
1633
1995
|
try {
|
|
1634
1996
|
workerDiscovery = await startNodeWorkerDiscovery(client);
|
|
1635
|
-
if (options.workerIndex === void 0) {
|
|
1636
|
-
const session = await initSession(client, target);
|
|
1997
|
+
if (options.workerIndex === void 0 && options.workerId === void 0) {
|
|
1998
|
+
const session = await initSession(client, target, { kind: "main" });
|
|
1637
1999
|
return withWorkerMetadata(session, workerDiscovery, targetIndex, targets.length);
|
|
1638
2000
|
}
|
|
1639
2001
|
return await initWorkerSession(
|
|
1640
2002
|
client,
|
|
1641
2003
|
workerDiscovery,
|
|
1642
2004
|
options.workerIndex,
|
|
2005
|
+
options.workerId,
|
|
1643
2006
|
targetIndex,
|
|
1644
2007
|
targets.length
|
|
1645
2008
|
);
|
|
@@ -1649,25 +2012,207 @@ async function connectInspector(options) {
|
|
|
1649
2012
|
throw err;
|
|
1650
2013
|
}
|
|
1651
2014
|
}
|
|
1652
|
-
async function initWorkerSession(parent, discovery, workerIndex, targetIndex, targetCount) {
|
|
1653
|
-
const workers = discovery.list();
|
|
2015
|
+
async function initWorkerSession(parent, discovery, workerIndex, workerId, targetIndex, targetCount) {
|
|
1654
2016
|
if (!discovery.supported) {
|
|
1655
2017
|
throw new CfInspectorError(
|
|
1656
2018
|
"INSPECTOR_DISCOVERY_FAILED",
|
|
1657
2019
|
"This runtime does not expose the NodeWorker CDP domain; --worker cannot be used. Run list-targets for available raw targets."
|
|
1658
2020
|
);
|
|
1659
2021
|
}
|
|
1660
|
-
const worker = workers[workerIndex];
|
|
2022
|
+
const worker = await discovery.waitFor((workers) => workerId === void 0 ? workerIndex === void 0 ? void 0 : workers[workerIndex] : workers.find((candidate) => candidate.workerId === workerId));
|
|
1661
2023
|
if (worker === void 0) {
|
|
2024
|
+
const workers = discovery.list();
|
|
2025
|
+
const selector = workerId === void 0 ? `index ${(workerIndex ?? 0).toString()}` : `workerId ${JSON.stringify(workerId)}`;
|
|
1662
2026
|
throw new CfInspectorError(
|
|
1663
2027
|
"INSPECTOR_DISCOVERY_FAILED",
|
|
1664
|
-
`No NodeWorker sub-session
|
|
2028
|
+
`No NodeWorker sub-session with ${selector} is currently attached (available: ${workers.length.toString()}). Ensure the worker is alive, then rerun list-targets.`
|
|
1665
2029
|
);
|
|
1666
2030
|
}
|
|
1667
2031
|
const client = await createNodeWorkerClient(parent, worker.sessionId);
|
|
1668
|
-
const session = await initSession(client, workerToInspectorTarget(worker)
|
|
2032
|
+
const session = await initSession(client, workerToInspectorTarget(worker), {
|
|
2033
|
+
kind: "worker",
|
|
2034
|
+
workerId: worker.workerId
|
|
2035
|
+
});
|
|
1669
2036
|
return withWorkerMetadata(session, discovery, targetIndex, targetCount, workerIndex, parent);
|
|
1670
2037
|
}
|
|
2038
|
+
async function connectInspectorGroup(options) {
|
|
2039
|
+
const host = options.host ?? DEFAULT_HOST;
|
|
2040
|
+
const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
2041
|
+
const targets = await discoverInspectorTargets(host, options.port, connectTimeoutMs);
|
|
2042
|
+
const targetIndex = options.targetIndex ?? 0;
|
|
2043
|
+
const target = targets[targetIndex];
|
|
2044
|
+
if (target === void 0) {
|
|
2045
|
+
throw new CfInspectorError(
|
|
2046
|
+
"INSPECTOR_DISCOVERY_FAILED",
|
|
2047
|
+
`No inspector target at index ${targetIndex.toString()} on ${host}:${options.port.toString()} (available: ${targets.length.toString()})`
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
const parent = await CdpClient.connect({ url: target.webSocketDebuggerUrl, connectTimeoutMs });
|
|
2051
|
+
let discovery;
|
|
2052
|
+
try {
|
|
2053
|
+
discovery = await startNodeWorkerDiscovery(parent);
|
|
2054
|
+
const main = await initSession(parent, target, { kind: "main" });
|
|
2055
|
+
const group = new DynamicInspectorSessionGroup(
|
|
2056
|
+
parent,
|
|
2057
|
+
discovery,
|
|
2058
|
+
main,
|
|
2059
|
+
targetIndex,
|
|
2060
|
+
targets.length
|
|
2061
|
+
);
|
|
2062
|
+
await group.initialize();
|
|
2063
|
+
return group;
|
|
2064
|
+
} catch (error) {
|
|
2065
|
+
await discovery?.dispose();
|
|
2066
|
+
parent.dispose();
|
|
2067
|
+
throw error;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
var DynamicInspectorSessionGroup = class {
|
|
2071
|
+
constructor(parent, discovery, main, targetIndex, targetCount) {
|
|
2072
|
+
this.parent = parent;
|
|
2073
|
+
this.discovery = discovery;
|
|
2074
|
+
this.targetIndex = targetIndex;
|
|
2075
|
+
this.targetCount = targetCount;
|
|
2076
|
+
this.workerDiscoverySupported = discovery.supported;
|
|
2077
|
+
this.sessions.set("main", main);
|
|
2078
|
+
this.detachDiscoveryListeners = [
|
|
2079
|
+
discovery.onAttached((worker) => {
|
|
2080
|
+
this.queueWorker(worker);
|
|
2081
|
+
}),
|
|
2082
|
+
discovery.onDetached((sessionId) => {
|
|
2083
|
+
void this.detachWorker(sessionId);
|
|
2084
|
+
})
|
|
2085
|
+
];
|
|
2086
|
+
}
|
|
2087
|
+
parent;
|
|
2088
|
+
discovery;
|
|
2089
|
+
targetIndex;
|
|
2090
|
+
targetCount;
|
|
2091
|
+
sessions = /* @__PURE__ */ new Map();
|
|
2092
|
+
listeners = /* @__PURE__ */ new Set();
|
|
2093
|
+
removedListeners = /* @__PURE__ */ new Set();
|
|
2094
|
+
errorListeners = /* @__PURE__ */ new Set();
|
|
2095
|
+
pending = /* @__PURE__ */ new Set();
|
|
2096
|
+
detachedSessionIds = /* @__PURE__ */ new Set();
|
|
2097
|
+
detachDiscoveryListeners;
|
|
2098
|
+
initializationError;
|
|
2099
|
+
initializing = true;
|
|
2100
|
+
disposed = false;
|
|
2101
|
+
workerDiscoverySupported;
|
|
2102
|
+
async initialize() {
|
|
2103
|
+
for (const worker of this.discovery.list()) {
|
|
2104
|
+
this.queueWorker(worker);
|
|
2105
|
+
}
|
|
2106
|
+
await this.waitForPending();
|
|
2107
|
+
this.initializing = false;
|
|
2108
|
+
if (this.initializationError !== void 0) {
|
|
2109
|
+
throw this.initializationError;
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
list() {
|
|
2113
|
+
return [...this.sessions.values()];
|
|
2114
|
+
}
|
|
2115
|
+
onSession(listener) {
|
|
2116
|
+
this.listeners.add(listener);
|
|
2117
|
+
for (const session of this.sessions.values()) {
|
|
2118
|
+
listener(session);
|
|
2119
|
+
}
|
|
2120
|
+
return () => {
|
|
2121
|
+
this.listeners.delete(listener);
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
onSessionRemoved(listener) {
|
|
2125
|
+
this.removedListeners.add(listener);
|
|
2126
|
+
return () => {
|
|
2127
|
+
this.removedListeners.delete(listener);
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
onError(listener) {
|
|
2131
|
+
this.errorListeners.add(listener);
|
|
2132
|
+
return () => {
|
|
2133
|
+
this.errorListeners.delete(listener);
|
|
2134
|
+
};
|
|
2135
|
+
}
|
|
2136
|
+
async dispose() {
|
|
2137
|
+
if (this.disposed) {
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
this.disposed = true;
|
|
2141
|
+
for (const detach of this.detachDiscoveryListeners) {
|
|
2142
|
+
detach();
|
|
2143
|
+
}
|
|
2144
|
+
await Promise.allSettled([...this.pending]);
|
|
2145
|
+
const sessions = [...this.list()].reverse();
|
|
2146
|
+
for (const session of sessions) {
|
|
2147
|
+
try {
|
|
2148
|
+
await session.dispose();
|
|
2149
|
+
} catch {
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
this.sessions.clear();
|
|
2153
|
+
this.listeners.clear();
|
|
2154
|
+
this.removedListeners.clear();
|
|
2155
|
+
this.errorListeners.clear();
|
|
2156
|
+
await this.discovery.dispose();
|
|
2157
|
+
this.parent.dispose();
|
|
2158
|
+
}
|
|
2159
|
+
queueWorker(worker) {
|
|
2160
|
+
if (this.disposed || this.sessions.has(worker.sessionId)) {
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
const pending = this.attachWorker(worker).catch((error) => {
|
|
2164
|
+
const normalized = error instanceof Error ? error : new Error("Worker inspector attachment failed");
|
|
2165
|
+
if (this.initializing && this.initializationError === void 0) {
|
|
2166
|
+
this.initializationError = normalized;
|
|
2167
|
+
}
|
|
2168
|
+
for (const listener of this.errorListeners) {
|
|
2169
|
+
listener(normalized);
|
|
2170
|
+
}
|
|
2171
|
+
}).finally(() => {
|
|
2172
|
+
this.pending.delete(pending);
|
|
2173
|
+
});
|
|
2174
|
+
this.pending.add(pending);
|
|
2175
|
+
}
|
|
2176
|
+
async attachWorker(worker) {
|
|
2177
|
+
const client = await createNodeWorkerClient(this.parent, worker.sessionId);
|
|
2178
|
+
try {
|
|
2179
|
+
const session = await initSession(client, workerToInspectorTarget(worker), {
|
|
2180
|
+
kind: "worker",
|
|
2181
|
+
workerId: worker.workerId
|
|
2182
|
+
});
|
|
2183
|
+
if (this.disposed || this.detachedSessionIds.delete(worker.sessionId)) {
|
|
2184
|
+
await session.dispose();
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
this.sessions.set(worker.sessionId, session);
|
|
2188
|
+
for (const listener of this.listeners) {
|
|
2189
|
+
listener(session);
|
|
2190
|
+
}
|
|
2191
|
+
} catch (error) {
|
|
2192
|
+
client.dispose();
|
|
2193
|
+
if (!this.disposed) {
|
|
2194
|
+
throw error;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
async detachWorker(sessionId) {
|
|
2199
|
+
const session = this.sessions.get(sessionId);
|
|
2200
|
+
if (session === void 0) {
|
|
2201
|
+
this.detachedSessionIds.add(sessionId);
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
this.sessions.delete(sessionId);
|
|
2205
|
+
for (const listener of this.removedListeners) {
|
|
2206
|
+
listener(session);
|
|
2207
|
+
}
|
|
2208
|
+
await session.dispose();
|
|
2209
|
+
}
|
|
2210
|
+
async waitForPending() {
|
|
2211
|
+
while (this.pending.size > 0) {
|
|
2212
|
+
await Promise.all([...this.pending]);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
};
|
|
1671
2216
|
function withWorkerMetadata(session, discovery, targetIndex, targetCount, workerIndex, parent) {
|
|
1672
2217
|
return {
|
|
1673
2218
|
...session,
|
|
@@ -1693,16 +2238,16 @@ function workerToInspectorTarget(worker) {
|
|
|
1693
2238
|
webSocketDebuggerUrl: `node-worker://${worker.sessionId}`
|
|
1694
2239
|
};
|
|
1695
2240
|
}
|
|
1696
|
-
async function initSession(client, target) {
|
|
2241
|
+
async function initSession(client, target, isolate) {
|
|
1697
2242
|
const scripts = /* @__PURE__ */ new Map();
|
|
1698
2243
|
registerScriptTracking(client, scripts);
|
|
1699
2244
|
const pauseBuffer = [];
|
|
1700
2245
|
const pauseWaitGate = { active: false };
|
|
1701
|
-
const debuggerState = {};
|
|
2246
|
+
const debuggerState = { paused: false };
|
|
1702
2247
|
registerPauseTracking(client, scripts, pauseBuffer, pauseWaitGate, debuggerState);
|
|
1703
2248
|
await client.send("Runtime.enable");
|
|
1704
2249
|
await client.send("Debugger.enable");
|
|
1705
|
-
return createSession(client, target, scripts, pauseBuffer, pauseWaitGate, debuggerState);
|
|
2250
|
+
return createSession(client, target, isolate, scripts, pauseBuffer, pauseWaitGate, debuggerState);
|
|
1706
2251
|
}
|
|
1707
2252
|
function registerScriptTracking(client, scripts) {
|
|
1708
2253
|
client.on("Debugger.scriptParsed", (raw) => {
|
|
@@ -1714,23 +2259,28 @@ function registerScriptTracking(client, scripts) {
|
|
|
1714
2259
|
}
|
|
1715
2260
|
function registerPauseTracking(client, scripts, pauseBuffer, pauseWaitGate, debuggerState) {
|
|
1716
2261
|
client.on("Debugger.paused", (raw) => {
|
|
2262
|
+
const event = toPauseEvent(raw, performance4.now(), scripts);
|
|
2263
|
+
debuggerState.paused = true;
|
|
2264
|
+
debuggerState.currentPause = event;
|
|
1717
2265
|
if (pauseWaitGate.active) {
|
|
1718
2266
|
return;
|
|
1719
2267
|
}
|
|
1720
|
-
const event = toPauseEvent(raw, performance3.now(), scripts);
|
|
1721
2268
|
if (pauseBuffer.length >= PAUSE_BUFFER_LIMIT) {
|
|
1722
2269
|
pauseBuffer.shift();
|
|
1723
2270
|
}
|
|
1724
2271
|
pauseBuffer.push(event);
|
|
1725
2272
|
});
|
|
1726
2273
|
client.on("Debugger.resumed", () => {
|
|
1727
|
-
debuggerState.
|
|
2274
|
+
debuggerState.paused = false;
|
|
2275
|
+
delete debuggerState.currentPause;
|
|
2276
|
+
debuggerState.lastResumedAtMs = performance4.now();
|
|
1728
2277
|
});
|
|
1729
2278
|
}
|
|
1730
|
-
function createSession(client, target, scripts, pauseBuffer, pauseWaitGate, debuggerState) {
|
|
2279
|
+
function createSession(client, target, isolate, scripts, pauseBuffer, pauseWaitGate, debuggerState) {
|
|
1731
2280
|
return {
|
|
1732
2281
|
client,
|
|
1733
2282
|
target,
|
|
2283
|
+
isolate,
|
|
1734
2284
|
scripts,
|
|
1735
2285
|
pauseBuffer,
|
|
1736
2286
|
pauseWaitGate,
|
|
@@ -2766,6 +3316,8 @@ async function waitForStop(session, options, registerMaxEventsSignal) {
|
|
|
2766
3316
|
|
|
2767
3317
|
// src/cf/tunnel.ts
|
|
2768
3318
|
import { startDebugger } from "@saptools/cf-debugger";
|
|
3319
|
+
init_types();
|
|
3320
|
+
var REUSED_TUNNEL_GRACE_MS = 5e3;
|
|
2769
3321
|
function targetOptions(target) {
|
|
2770
3322
|
return {
|
|
2771
3323
|
...target.apiEndpoint === void 0 ? {} : { apiEndpoint: target.apiEndpoint },
|
|
@@ -2827,11 +3379,22 @@ async function openCfTunnel(target) {
|
|
|
2827
3379
|
if (localPort === void 0) {
|
|
2828
3380
|
throw error;
|
|
2829
3381
|
}
|
|
3382
|
+
try {
|
|
3383
|
+
await fetchInspectorVersion("127.0.0.1", localPort, REUSED_TUNNEL_GRACE_MS);
|
|
3384
|
+
} catch (livenessError) {
|
|
3385
|
+
const detail = livenessError instanceof Error ? livenessError.message : String(livenessError);
|
|
3386
|
+
throw new CfInspectorError(
|
|
3387
|
+
"INSPECTOR_DISCOVERY_FAILED",
|
|
3388
|
+
`Another debugger session claims tunnel port ${localPort.toString()}, but its Node inspector did not respond within ${REUSED_TUNNEL_GRACE_MS.toString()}ms. The tunnel may be stale or still finishing setup; retry shortly, or inspect and stop the owning cf-debugger session before opening a fresh tunnel.`,
|
|
3389
|
+
detail
|
|
3390
|
+
);
|
|
3391
|
+
}
|
|
2830
3392
|
target.onStatus?.("ready", `Reusing existing tunnel on port ${localPort.toString()}`);
|
|
2831
3393
|
return { localPort, dispose: () => Promise.resolve() };
|
|
2832
3394
|
}
|
|
2833
3395
|
}
|
|
2834
3396
|
export {
|
|
3397
|
+
BreakpointFanout,
|
|
2835
3398
|
CfInspectorError,
|
|
2836
3399
|
buildBreakpointUrlRegex,
|
|
2837
3400
|
buildHitCountedCondition,
|
|
@@ -2839,6 +3402,7 @@ export {
|
|
|
2839
3402
|
captureException,
|
|
2840
3403
|
captureSnapshot,
|
|
2841
3404
|
connectInspector,
|
|
3405
|
+
connectInspectorGroup,
|
|
2842
3406
|
discoverInspectorTargets,
|
|
2843
3407
|
evaluateGlobal,
|
|
2844
3408
|
evaluateOnFrame,
|
|
@@ -2860,6 +3424,7 @@ export {
|
|
|
2860
3424
|
setBreakpoint,
|
|
2861
3425
|
setBreakpointAtLocation,
|
|
2862
3426
|
setPauseOnExceptions,
|
|
3427
|
+
startInspectorKeepalive,
|
|
2863
3428
|
stepInto,
|
|
2864
3429
|
stepOut,
|
|
2865
3430
|
stepOver,
|