@saptools/cf-inspector 0.2.1 → 0.3.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 +12 -8
- package/dist/cli.js +67 -19
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +31 -2
- package/dist/index.js +48 -17
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ export SAP_PASSWORD=...
|
|
|
69
69
|
cf-inspector snapshot \
|
|
70
70
|
--region eu10 --org my-org --space dev --app my-srv \
|
|
71
71
|
--bp src/handler.ts:42 \
|
|
72
|
-
--remote-root 'regex:^/(home/vcap/app|
|
|
72
|
+
--remote-root 'regex:^/(home/vcap/app|example-root-.*)$'
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
The first form connects directly to `localhost:9229`. The second internally calls `@saptools/cf-debugger` to open the SSH tunnel, runs the snapshot through it, and tears the tunnel down on exit.
|
|
@@ -233,14 +233,18 @@ console.log({ bp, snapshot, customValue });
|
|
|
233
233
|
|
|
234
234
|
| Code | When |
|
|
235
235
|
| --- | --- |
|
|
236
|
-
| `
|
|
236
|
+
| `INVALID_ARGUMENT` | A numeric flag (`--port`, `--timeout`, `--duration`, …) is not a positive integer |
|
|
237
|
+
| `INVALID_BREAKPOINT` | `--bp` / `--at` is not in `file:line` form, or line is not a positive integer |
|
|
237
238
|
| `INVALID_REMOTE_ROOT` | `--remote-root` regex did not compile |
|
|
239
|
+
| `INVALID_EXPRESSION` | `--condition` or `--expr` failed to parse on V8 (`Runtime.compileScript` reported a SyntaxError) — fast-fail before the breakpoint is set |
|
|
240
|
+
| `BREAKPOINT_DID_NOT_BIND` | Reserved: a breakpoint resolved to no scripts. Currently surfaced as a stderr warning only — see `BreakpointHandle.resolvedLocations` for programmatic detection |
|
|
238
241
|
| `INSPECTOR_DISCOVERY_FAILED` | `/json/list` did not return a usable WebSocket URL |
|
|
239
|
-
| `INSPECTOR_CONNECTION_FAILED` | WebSocket handshake failed |
|
|
240
|
-
| `CDP_REQUEST_FAILED` | A CDP method returned an error result |
|
|
242
|
+
| `INSPECTOR_CONNECTION_FAILED` | WebSocket handshake failed, or the connection closed mid-request |
|
|
243
|
+
| `CDP_REQUEST_FAILED` | A CDP method returned an error result, timed out, or failed to send |
|
|
241
244
|
| `BREAKPOINT_NOT_HIT` | The breakpoint did not hit before the timeout elapsed |
|
|
242
|
-
| `EVALUATION_FAILED` |
|
|
243
|
-
| `MISSING_TARGET` | Neither `--port` nor a CF target was provided |
|
|
245
|
+
| `EVALUATION_FAILED` | Reserved for future use — current evaluation paths surface remote exceptions inline via `CapturedExpression.error` instead of throwing |
|
|
246
|
+
| `MISSING_TARGET` | Neither `--port` nor a complete CF target (`--region/--org/--space/--app`) was provided |
|
|
247
|
+
| `ABORTED` | Reserved for future use by long-running streams when an `AbortSignal` fires |
|
|
244
248
|
|
|
245
249
|
</details>
|
|
246
250
|
|
|
@@ -265,8 +269,8 @@ Path mapping uses CDP's first-class `urlRegex`:
|
|
|
265
269
|
| --- | --- |
|
|
266
270
|
| _omitted_ | `(?:^|/)src/handler\.(?:ts\|js)$` |
|
|
267
271
|
| `/home/vcap/app` (literal) | `^file:///home/vcap/app/src/handler\.(?:ts\|js)$` |
|
|
268
|
-
| `regex:^/
|
|
269
|
-
| `/^/
|
|
272
|
+
| `regex:^/example-root-.*$` | `^file:///example-root-[^/]+/src/handler\.(?:ts\|js)$` |
|
|
273
|
+
| `/^/example-root-.*$/` | same as above |
|
|
270
274
|
|
|
271
275
|
`.ts ↔ .js` is folded into the regex automatically because Node's V8 inspector normally serves both the source-mapped TypeScript URL and the runtime JavaScript URL — matching either is correct.
|
|
272
276
|
|
package/dist/cli.js
CHANGED
|
@@ -606,7 +606,11 @@ async function connectInspector(options) {
|
|
|
606
606
|
});
|
|
607
607
|
const PAUSE_BUFFER_LIMIT = 32;
|
|
608
608
|
const pauseBuffer = [];
|
|
609
|
+
const pauseWaitGate = { active: false };
|
|
609
610
|
client.on("Debugger.paused", (raw) => {
|
|
611
|
+
if (pauseWaitGate.active) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
610
614
|
const params = raw;
|
|
611
615
|
const event = {
|
|
612
616
|
reason: asString(params.reason),
|
|
@@ -625,6 +629,7 @@ async function connectInspector(options) {
|
|
|
625
629
|
target,
|
|
626
630
|
scripts,
|
|
627
631
|
pauseBuffer,
|
|
632
|
+
pauseWaitGate,
|
|
628
633
|
dispose: async () => {
|
|
629
634
|
try {
|
|
630
635
|
await client.send("Debugger.disable");
|
|
@@ -750,17 +755,23 @@ async function waitForPause(session, options) {
|
|
|
750
755
|
return head;
|
|
751
756
|
}
|
|
752
757
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
758
|
+
session.pauseWaitGate.active = true;
|
|
759
|
+
let params;
|
|
760
|
+
try {
|
|
761
|
+
const expected = options.breakpointIds;
|
|
762
|
+
params = await session.client.waitFor("Debugger.paused", {
|
|
763
|
+
timeoutMs: options.timeoutMs,
|
|
764
|
+
predicate: (raw) => {
|
|
765
|
+
if (expected === void 0 || expected.length === 0) {
|
|
766
|
+
return true;
|
|
767
|
+
}
|
|
768
|
+
const ids = Array.isArray(raw.hitBreakpoints) ? raw.hitBreakpoints.filter((id) => typeof id === "string") : [];
|
|
769
|
+
return ids.some((id) => expected.includes(id));
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
} finally {
|
|
773
|
+
session.pauseWaitGate.active = false;
|
|
774
|
+
}
|
|
764
775
|
return {
|
|
765
776
|
reason: asString(params.reason),
|
|
766
777
|
hitBreakpoints: Array.isArray(params.hitBreakpoints) ? params.hitBreakpoints.filter((id) => typeof id === "string") : [],
|
|
@@ -790,6 +801,18 @@ async function evaluateGlobal(session, expression) {
|
|
|
790
801
|
function listScripts(session) {
|
|
791
802
|
return [...session.scripts.values()];
|
|
792
803
|
}
|
|
804
|
+
async function validateExpression(session, expression) {
|
|
805
|
+
const result = await session.client.send("Runtime.compileScript", {
|
|
806
|
+
expression,
|
|
807
|
+
sourceURL: "<cf-inspector-validate>",
|
|
808
|
+
persistScript: false
|
|
809
|
+
});
|
|
810
|
+
if (result.exceptionDetails === void 0) {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const description = typeof result.exceptionDetails.exception?.description === "string" ? result.exceptionDetails.exception.description : typeof result.exceptionDetails.text === "string" ? result.exceptionDetails.text : "expression failed to compile";
|
|
814
|
+
throw new CfInspectorError("INVALID_EXPRESSION", description);
|
|
815
|
+
}
|
|
793
816
|
async function getProperties(session, objectId) {
|
|
794
817
|
const result = await session.client.send("Runtime.getProperties", {
|
|
795
818
|
objectId,
|
|
@@ -870,12 +893,6 @@ function generateSentinel() {
|
|
|
870
893
|
async function streamLogpoint(session, options) {
|
|
871
894
|
const sentinel = generateSentinel();
|
|
872
895
|
const condition = buildLogpointCondition(sentinel, options.expression);
|
|
873
|
-
const handle = await setBreakpoint(session, {
|
|
874
|
-
file: options.location.file,
|
|
875
|
-
line: options.location.line,
|
|
876
|
-
...options.remoteRoot === void 0 ? {} : { remoteRoot: options.remoteRoot },
|
|
877
|
-
condition
|
|
878
|
-
});
|
|
879
896
|
let emitted = 0;
|
|
880
897
|
const offEvent = session.client.on("Runtime.consoleAPICalled", (raw) => {
|
|
881
898
|
const params = raw;
|
|
@@ -890,6 +907,19 @@ async function streamLogpoint(session, options) {
|
|
|
890
907
|
emitted += 1;
|
|
891
908
|
options.onEvent(event);
|
|
892
909
|
});
|
|
910
|
+
let handle;
|
|
911
|
+
try {
|
|
912
|
+
handle = await setBreakpoint(session, {
|
|
913
|
+
file: options.location.file,
|
|
914
|
+
line: options.location.line,
|
|
915
|
+
...options.remoteRoot === void 0 ? {} : { remoteRoot: options.remoteRoot },
|
|
916
|
+
condition
|
|
917
|
+
});
|
|
918
|
+
} catch (err) {
|
|
919
|
+
offEvent();
|
|
920
|
+
throw err;
|
|
921
|
+
}
|
|
922
|
+
options.onBreakpointSet?.(handle);
|
|
893
923
|
const cleanup = async () => {
|
|
894
924
|
offEvent();
|
|
895
925
|
try {
|
|
@@ -1164,8 +1194,8 @@ function parsePositiveInt(raw, label) {
|
|
|
1164
1194
|
return void 0;
|
|
1165
1195
|
}
|
|
1166
1196
|
const value = Number.parseInt(raw, 10);
|
|
1167
|
-
if (Number.isNaN(value) || value <= 0) {
|
|
1168
|
-
throw new CfInspectorError("
|
|
1197
|
+
if (Number.isNaN(value) || value <= 0 || value.toString() !== raw.trim()) {
|
|
1198
|
+
throw new CfInspectorError("INVALID_ARGUMENT", `Invalid ${label}: "${raw}" \u2014 expected a positive integer`);
|
|
1169
1199
|
}
|
|
1170
1200
|
return value;
|
|
1171
1201
|
}
|
|
@@ -1236,6 +1266,16 @@ function writeJson(value) {
|
|
|
1236
1266
|
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
1237
1267
|
`);
|
|
1238
1268
|
}
|
|
1269
|
+
function warnOnUnboundBreakpoints(handles) {
|
|
1270
|
+
for (const handle of handles) {
|
|
1271
|
+
if (handle.resolvedLocations.length === 0) {
|
|
1272
|
+
process.stderr.write(
|
|
1273
|
+
`[cf-inspector] warning: breakpoint ${handle.file}:${handle.line.toString()} did not bind to any loaded script. Check the path or pass --remote-root. Use 'list-scripts' to inspect what V8 currently has loaded.
|
|
1274
|
+
`
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1239
1279
|
function writeHumanSnapshot(snapshot) {
|
|
1240
1280
|
const lines = [];
|
|
1241
1281
|
lines.push(`Snapshot @ ${snapshot.capturedAt}`, ` reason: ${snapshot.reason}`);
|
|
@@ -1278,6 +1318,9 @@ async function handleSnapshot(opts) {
|
|
|
1278
1318
|
const timeoutMs = timeoutSec * 1e3;
|
|
1279
1319
|
const condition = opts.condition !== void 0 && opts.condition.trim().length > 0 ? opts.condition.trim() : void 0;
|
|
1280
1320
|
const result = await withSession(target, async (session) => {
|
|
1321
|
+
if (condition !== void 0) {
|
|
1322
|
+
await validateExpression(session, condition);
|
|
1323
|
+
}
|
|
1281
1324
|
const handles = await Promise.all(
|
|
1282
1325
|
breakpoints.map(
|
|
1283
1326
|
(bp) => setBreakpoint(session, {
|
|
@@ -1288,6 +1331,7 @@ async function handleSnapshot(opts) {
|
|
|
1288
1331
|
})
|
|
1289
1332
|
)
|
|
1290
1333
|
);
|
|
1334
|
+
warnOnUnboundBreakpoints(handles);
|
|
1291
1335
|
const breakpointIds = handles.map((h) => h.breakpointId);
|
|
1292
1336
|
const pause = await waitForPause(session, { timeoutMs, breakpointIds });
|
|
1293
1337
|
const snapshot = await captureSnapshot(session, pause, { captures });
|
|
@@ -1370,6 +1414,7 @@ async function handleLog(opts) {
|
|
|
1370
1414
|
process.once("SIGTERM", onSig);
|
|
1371
1415
|
try {
|
|
1372
1416
|
await withSession(target, async (session) => {
|
|
1417
|
+
await validateExpression(session, expression);
|
|
1373
1418
|
const result = await streamLogpoint(session, {
|
|
1374
1419
|
location,
|
|
1375
1420
|
expression,
|
|
@@ -1378,6 +1423,9 @@ async function handleLog(opts) {
|
|
|
1378
1423
|
signal: abort.signal,
|
|
1379
1424
|
onEvent: (event) => {
|
|
1380
1425
|
writeLogEvent(event, opts.json);
|
|
1426
|
+
},
|
|
1427
|
+
onBreakpointSet: (handle) => {
|
|
1428
|
+
warnOnUnboundBreakpoints([handle]);
|
|
1381
1429
|
}
|
|
1382
1430
|
});
|
|
1383
1431
|
if (opts.json) {
|