@saptools/cf-inspector 0.2.2 → 0.3.1

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 CHANGED
@@ -115,6 +115,10 @@ cf-inspector snapshot --port 9229 \
115
115
  | `--no-json` | Print a human-readable summary instead of JSON |
116
116
  | `--keep-paused` | Skip the auto-resume after capture (useful for diagnostics) |
117
117
 
118
+ JSON output includes `captureDurationMs`, the time spent collecting scopes and
119
+ `--capture` expressions after the breakpoint has paused the process. It does
120
+ not include the time spent waiting for the breakpoint to hit.
121
+
118
122
  For Cloud Foundry targets, replace `--port` with `--region/--org/--space/--app` (and optionally `--cf-timeout <seconds>` for the tunnel).
119
123
 
120
124
  ### 📡 `cf-inspector log`
@@ -233,14 +237,18 @@ console.log({ bp, snapshot, customValue });
233
237
 
234
238
  | Code | When |
235
239
  | --- | --- |
236
- | `INVALID_BREAKPOINT` | `--bp` is not in `file:line` form, or line is not a positive integer |
240
+ | `INVALID_ARGUMENT` | A numeric flag (`--port`, `--timeout`, `--duration`, …) is not a positive integer |
241
+ | `INVALID_BREAKPOINT` | `--bp` / `--at` is not in `file:line` form, or line is not a positive integer |
237
242
  | `INVALID_REMOTE_ROOT` | `--remote-root` regex did not compile |
243
+ | `INVALID_EXPRESSION` | `--condition` or `--expr` failed to parse on V8 (`Runtime.compileScript` reported a SyntaxError) — fast-fail before the breakpoint is set |
244
+ | `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
245
  | `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 |
246
+ | `INSPECTOR_CONNECTION_FAILED` | WebSocket handshake failed, or the connection closed mid-request |
247
+ | `CDP_REQUEST_FAILED` | A CDP method returned an error result, timed out, or failed to send |
241
248
  | `BREAKPOINT_NOT_HIT` | The breakpoint did not hit before the timeout elapsed |
242
- | `EVALUATION_FAILED` | `Runtime.evaluate` / `Debugger.evaluateOnCallFrame` returned a remote exception |
243
- | `MISSING_TARGET` | Neither `--port` nor a CF target was provided |
249
+ | `EVALUATION_FAILED` | Reserved for future use — current evaluation paths surface remote exceptions inline via `CapturedExpression.error` instead of throwing |
250
+ | `MISSING_TARGET` | Neither `--port` nor a complete CF target (`--region/--org/--space/--app`) was provided |
251
+ | `ABORTED` | Reserved for future use by long-running streams when an `AbortSignal` fires |
244
252
 
245
253
  </details>
246
254
 
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
- const params = await session.client.waitFor("Debugger.paused", {
754
- timeoutMs: options.timeoutMs,
755
- predicate: (raw) => {
756
- const event = {
757
- reason: asString(raw.reason),
758
- hitBreakpoints: Array.isArray(raw.hitBreakpoints) ? raw.hitBreakpoints.filter((id) => typeof id === "string") : [],
759
- callFrames: []
760
- };
761
- return pauseMatches(event, options.breakpointIds);
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 {
@@ -939,6 +969,7 @@ async function waitForStop(session, options) {
939
969
  }
940
970
 
941
971
  // src/snapshot.ts
972
+ import { performance } from "perf_hooks";
942
973
  var MAX_SCOPES = 3;
943
974
  var MAX_SCOPE_VARIABLES = 20;
944
975
  var MAX_CHILD_VARIABLES = 8;
@@ -1097,6 +1128,7 @@ function evalResultToCaptured(expression, result) {
1097
1128
  return buildCaptured("undefined");
1098
1129
  }
1099
1130
  async function captureSnapshot(session, pause, options = {}) {
1131
+ const startedAt = performance.now();
1100
1132
  const top = pause.callFrames[0];
1101
1133
  let topFrame;
1102
1134
  let captures = [];
@@ -1123,10 +1155,12 @@ async function captureSnapshot(session, pause, options = {}) {
1123
1155
  );
1124
1156
  }
1125
1157
  }
1158
+ const captureDurationMs = Math.round((performance.now() - startedAt) * 1e3) / 1e3;
1126
1159
  return {
1127
1160
  reason: pause.reason,
1128
1161
  hitBreakpoints: pause.hitBreakpoints,
1129
1162
  capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
1163
+ captureDurationMs,
1130
1164
  ...topFrame === void 0 ? {} : { topFrame },
1131
1165
  captures
1132
1166
  };
@@ -1164,8 +1198,8 @@ function parsePositiveInt(raw, label) {
1164
1198
  return void 0;
1165
1199
  }
1166
1200
  const value = Number.parseInt(raw, 10);
1167
- if (Number.isNaN(value) || value <= 0) {
1168
- throw new CfInspectorError("MISSING_TARGET", `Invalid ${label}: "${raw}"`);
1201
+ if (Number.isNaN(value) || value <= 0 || value.toString() !== raw.trim()) {
1202
+ throw new CfInspectorError("INVALID_ARGUMENT", `Invalid ${label}: "${raw}" \u2014 expected a positive integer`);
1169
1203
  }
1170
1204
  return value;
1171
1205
  }
@@ -1236,9 +1270,23 @@ function writeJson(value) {
1236
1270
  process.stdout.write(`${JSON.stringify(value, null, 2)}
1237
1271
  `);
1238
1272
  }
1273
+ function warnOnUnboundBreakpoints(handles) {
1274
+ for (const handle of handles) {
1275
+ if (handle.resolvedLocations.length === 0) {
1276
+ process.stderr.write(
1277
+ `[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.
1278
+ `
1279
+ );
1280
+ }
1281
+ }
1282
+ }
1239
1283
  function writeHumanSnapshot(snapshot) {
1240
1284
  const lines = [];
1241
- lines.push(`Snapshot @ ${snapshot.capturedAt}`, ` reason: ${snapshot.reason}`);
1285
+ lines.push(
1286
+ `Snapshot @ ${snapshot.capturedAt}`,
1287
+ ` reason: ${snapshot.reason}`,
1288
+ ` capture: ${snapshot.captureDurationMs.toFixed(1)}ms`
1289
+ );
1242
1290
  if (snapshot.topFrame) {
1243
1291
  const frame = snapshot.topFrame;
1244
1292
  const fnName = frame.functionName.length === 0 ? "(anonymous)" : frame.functionName;
@@ -1278,6 +1326,9 @@ async function handleSnapshot(opts) {
1278
1326
  const timeoutMs = timeoutSec * 1e3;
1279
1327
  const condition = opts.condition !== void 0 && opts.condition.trim().length > 0 ? opts.condition.trim() : void 0;
1280
1328
  const result = await withSession(target, async (session) => {
1329
+ if (condition !== void 0) {
1330
+ await validateExpression(session, condition);
1331
+ }
1281
1332
  const handles = await Promise.all(
1282
1333
  breakpoints.map(
1283
1334
  (bp) => setBreakpoint(session, {
@@ -1288,6 +1339,7 @@ async function handleSnapshot(opts) {
1288
1339
  })
1289
1340
  )
1290
1341
  );
1342
+ warnOnUnboundBreakpoints(handles);
1291
1343
  const breakpointIds = handles.map((h) => h.breakpointId);
1292
1344
  const pause = await waitForPause(session, { timeoutMs, breakpointIds });
1293
1345
  const snapshot = await captureSnapshot(session, pause, { captures });
@@ -1370,6 +1422,7 @@ async function handleLog(opts) {
1370
1422
  process.once("SIGTERM", onSig);
1371
1423
  try {
1372
1424
  await withSession(target, async (session) => {
1425
+ await validateExpression(session, expression);
1373
1426
  const result = await streamLogpoint(session, {
1374
1427
  location,
1375
1428
  expression,
@@ -1378,6 +1431,9 @@ async function handleLog(opts) {
1378
1431
  signal: abort.signal,
1379
1432
  onEvent: (event) => {
1380
1433
  writeLogEvent(event, opts.json);
1434
+ },
1435
+ onBreakpointSet: (handle) => {
1436
+ warnOnUnboundBreakpoints([handle]);
1381
1437
  }
1382
1438
  });
1383
1439
  if (opts.json) {