@saptools/cf-live-trace 0.1.4 → 0.1.6

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/dist/cli.js CHANGED
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/cli.ts
4
+ import process4 from "process";
5
+
3
6
  // src/cli/program.ts
4
7
  import process3 from "process";
5
8
  import { Command } from "commander";
@@ -52,16 +55,16 @@ async function ensureSshEnabled(target, dependencies = defaultCfDependencies) {
52
55
  timeoutMs: CF_SSH_READY_TIMEOUT_MS
53
56
  });
54
57
  }
55
- async function tryStartNodeInspector(target, dependencies = defaultCfDependencies) {
58
+ async function startNodeInspector(target, dependencies = defaultCfDependencies) {
59
+ const redactor = createSecretRedactor([target.email, target.password]);
56
60
  try {
57
- const redactor = createSecretRedactor([target.email, target.password]);
58
61
  const stdout = await dependencies.runCf(
59
62
  buildCfSshArgs(target.app, target.instanceIndex, ["-c", buildInspectorSignalCommand()]),
60
63
  { ...buildRunOptions(target, redactor), timeoutMs: INSPECTOR_SIGNAL_TIMEOUT_MS }
61
64
  );
62
- return hasInspectorReadyMarker(stdout);
63
- } catch {
64
- return false;
65
+ return hasInspectorReadyMarker(stdout) ? { status: "ready" } : { status: "not-ready", ...optionalDetail(summarizeInspectorStartupOutput(stdout)) };
66
+ } catch (error) {
67
+ return { status: "not-ready", ...optionalDetail(redactor(formatErrorMessage(error))) };
65
68
  }
66
69
  }
67
70
  async function openInspectorTunnel(target, dependencies = defaultTunnelDependencies) {
@@ -109,6 +112,8 @@ function spawnPortForward(params) {
109
112
  env: buildCfEnv(params.cfHomeDir),
110
113
  stdio: ["ignore", "pipe", "pipe"]
111
114
  });
115
+ child.stdout.resume();
116
+ child.stderr.resume();
112
117
  return {
113
118
  process: child,
114
119
  localPort: params.localPort,
@@ -216,6 +221,7 @@ function resolveTunnelAppName(target) {
216
221
  return appName;
217
222
  }
218
223
  async function raceForwardReadiness(handle, dependencies) {
224
+ const controller = new AbortController();
219
225
  let markFailed = () => {
220
226
  return;
221
227
  };
@@ -226,11 +232,14 @@ async function raceForwardReadiness(handle, dependencies) {
226
232
  handle.process.once("exit", markFailed);
227
233
  handle.process.once("error", markFailed);
228
234
  });
229
- const ready = dependencies.waitForLocalPort(handle.localPort, TUNNEL_READY_TIMEOUT_MS);
230
- const outcome = await Promise.race([ready, failedEarly]);
231
- handle.process.removeListener("exit", markFailed);
232
- handle.process.removeListener("error", markFailed);
233
- return outcome;
235
+ const ready = dependencies.waitForLocalPort(handle.localPort, TUNNEL_READY_TIMEOUT_MS, controller.signal);
236
+ try {
237
+ return await Promise.race([ready, failedEarly]);
238
+ } finally {
239
+ controller.abort();
240
+ handle.process.removeListener("exit", markFailed);
241
+ handle.process.removeListener("error", markFailed);
242
+ }
234
243
  }
235
244
  function resolveCommand(command) {
236
245
  const resolvedBin = command ?? process.env["CF_LIVE_TRACE_CF_BIN"] ?? "cf";
@@ -258,7 +267,15 @@ function extractErrorDetail(error) {
258
267
  if (stderr.length > 0) {
259
268
  return stderr;
260
269
  }
261
- return error["message"] instanceof Error ? error["message"].message : "";
270
+ const message = error["message"];
271
+ if (typeof message === "string") {
272
+ return message.trim();
273
+ }
274
+ return message instanceof Error ? message.message.trim() : "";
275
+ }
276
+ function formatErrorMessage(error) {
277
+ const message = error instanceof Error ? error.message : String(error);
278
+ return message.trim().length > 0 ? message.trim() : "Unknown error";
262
279
  }
263
280
  function formatArgs(args) {
264
281
  return args.join(" ");
@@ -266,6 +283,13 @@ function formatArgs(args) {
266
283
  function hasInspectorReadyMarker(stdout) {
267
284
  return stdout.split(/\r?\n/).map((line) => line.trim()).includes("saptools-inspector-ready");
268
285
  }
286
+ function summarizeInspectorStartupOutput(stdout) {
287
+ const markers = stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("saptools-inspector-"));
288
+ return markers.length === 0 ? void 0 : markers.join("; ");
289
+ }
290
+ function optionalDetail(detail) {
291
+ return detail === void 0 || detail.length === 0 ? {} : { detail };
292
+ }
269
293
  function findFreePort() {
270
294
  return new Promise((resolve, reject) => {
271
295
  const server = createServer();
@@ -283,29 +307,61 @@ function findFreePort() {
283
307
  });
284
308
  });
285
309
  }
286
- function waitForLocalPort(port, timeoutMs) {
310
+ function waitForLocalPort(port, timeoutMs, signal) {
287
311
  const deadline = Date.now() + timeoutMs;
288
312
  return new Promise((resolve) => {
313
+ let activeSocket;
314
+ let settled = false;
315
+ let retryTimer;
316
+ const finish = (ready) => {
317
+ if (settled) {
318
+ return;
319
+ }
320
+ settled = true;
321
+ if (retryTimer !== void 0) {
322
+ clearTimeout(retryTimer);
323
+ }
324
+ activeSocket?.destroy();
325
+ activeSocket = void 0;
326
+ signal?.removeEventListener("abort", onAbort);
327
+ resolve(ready);
328
+ };
329
+ const onAbort = () => {
330
+ finish(false);
331
+ };
289
332
  const attempt = () => {
333
+ if (settled) {
334
+ return;
335
+ }
290
336
  const socket = netConnect({ host: "127.0.0.1", port });
337
+ activeSocket = socket;
291
338
  socket.once("connect", () => {
292
339
  socket.destroy();
293
- resolve(true);
340
+ activeSocket = void 0;
341
+ finish(true);
294
342
  });
295
343
  socket.once("error", () => {
296
- retryPortProbe(socket, deadline, attempt, resolve);
344
+ activeSocket = void 0;
345
+ retryPortProbe(socket, deadline, attempt, finish, (timer) => {
346
+ retryTimer = timer;
347
+ });
297
348
  });
298
349
  };
350
+ if (signal?.aborted === true) {
351
+ finish(false);
352
+ return;
353
+ }
354
+ signal?.addEventListener("abort", onAbort, { once: true });
299
355
  attempt();
300
356
  });
301
357
  }
302
- function retryPortProbe(socket, deadline, attempt, resolve) {
358
+ function retryPortProbe(socket, deadline, attempt, finish, setRetryTimer) {
303
359
  socket.destroy();
304
360
  if (Date.now() >= deadline) {
305
- resolve(false);
361
+ finish(false);
306
362
  return;
307
363
  }
308
- setTimeout(attempt, TUNNEL_READY_POLL_MS);
364
+ setRetryTimer(setTimeout(attempt, TUNNEL_READY_POLL_MS));
309
365
  }
310
366
  function isRecord(value) {
311
367
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -579,7 +635,7 @@ function isRecord2(value) {
579
635
 
580
636
  // src/runtime-source.ts
581
637
  var CF_LIVE_TRACE_GLOBAL_NAME = "__SAPTOOLS_CF_LIVE_TRACE__";
582
- var CF_LIVE_TRACE_RUNTIME_VERSION = 1;
638
+ var CF_LIVE_TRACE_RUNTIME_VERSION = 2;
583
639
  var CF_LIVE_TRACE_RUNTIME_SOURCE = `
584
640
  (() => {
585
641
  const name = '${CF_LIVE_TRACE_GLOBAL_NAME}';
@@ -776,15 +832,12 @@ var CF_LIVE_TRACE_RUNTIME_SOURCE = `
776
832
  status() {
777
833
  return { installed: state.installed, enabled: state.enabled, queueSize: state.queue.length, droppedCount: state.droppedCount, maxEvents: state.options.maxEvents };
778
834
  },
779
- uninstall() {
835
+ async uninstall() {
780
836
  state.enabled = false;
781
- const requireFn = loadRequire();
782
- if (requireFn) {
783
- const http = requireFn('http');
784
- const https = requireFn('https');
785
- if (state.originals.httpServerEmit && http && http.Server) http.Server.prototype.emit = state.originals.httpServerEmit;
786
- if (state.originals.httpsServerEmit && https && https.Server) https.Server.prototype.emit = state.originals.httpsServerEmit;
787
- }
837
+ const http = await loadModule('http');
838
+ const https = await loadModule('https');
839
+ if (state.originals.httpServerEmit && http && http.Server) http.Server.prototype.emit = state.originals.httpServerEmit;
840
+ if (state.originals.httpsServerEmit && https && https.Server) https.Server.prototype.emit = state.originals.httpsServerEmit;
788
841
  state.installed = false;
789
842
  return api.status();
790
843
  }
@@ -893,7 +946,7 @@ var DRAIN_TRANSPORT_BODY_LIMIT = 2e4;
893
946
  var defaultDependencies = {
894
947
  prepareCfSession,
895
948
  ensureSshEnabled,
896
- tryStartNodeInspector,
949
+ tryStartNodeInspector: startNodeInspector,
897
950
  openInspectorTunnel,
898
951
  connectInspector: connectRuntimeInspector,
899
952
  setInterval,
@@ -924,6 +977,9 @@ var LiveTraceSession = class {
924
977
  async stop(options) {
925
978
  this.stopRequested = true;
926
979
  if (!this.isRunning()) {
980
+ if (this.state === "error" && options.reason === "error") {
981
+ return;
982
+ }
927
983
  this.postState("stopped", `Trace stopped (${options.reason}).`, false, false);
928
984
  return;
929
985
  }
@@ -946,9 +1002,11 @@ var LiveTraceSession = class {
946
1002
  } catch (error) {
947
1003
  this.log(`Live Trace startup failed for ${this.options.target.app}: ${formatError(error)}`);
948
1004
  await this.stopRuntimeTrace(false);
1005
+ const startupError = toStartupError(error);
949
1006
  if (!this.shouldStop()) {
950
- this.postState("error", "Runtime HTTP trace could not be started.", false, false);
1007
+ this.postState("error", startupError.stateMessage, false, false);
951
1008
  }
1009
+ throw startupError;
952
1010
  }
953
1011
  }
954
1012
  async startInspector(options) {
@@ -958,7 +1016,7 @@ var LiveTraceSession = class {
958
1016
  return;
959
1017
  }
960
1018
  this.postState("starting-inspector", "Requesting Node Inspector startup.", false, false);
961
- await this.dependencies.tryStartNodeInspector(this.options.target);
1019
+ this.reportInspectorStartup(await this.dependencies.tryStartNodeInspector(this.options.target));
962
1020
  if (this.shouldStop()) {
963
1021
  return;
964
1022
  }
@@ -972,8 +1030,10 @@ var LiveTraceSession = class {
972
1030
  return;
973
1031
  }
974
1032
  if (tunnel.status !== "ready") {
975
- this.postState("error", "Node Inspector is not reachable on 127.0.0.1:9229.", false, false);
976
- return;
1033
+ throw new LiveTraceStartupError(
1034
+ "Node Inspector is not reachable on 127.0.0.1:9229.",
1035
+ "Node Inspector is not reachable on 127.0.0.1:9229."
1036
+ );
977
1037
  }
978
1038
  this.tunnelHandle = tunnel.handle;
979
1039
  this.inspectorClient = await this.dependencies.connectInspector(tunnel.handle.localPort);
@@ -1044,11 +1104,27 @@ var LiveTraceSession = class {
1044
1104
  this.stopPolling();
1045
1105
  this.consecutiveDrainTimeouts = 0;
1046
1106
  const uninstalled = await this.stopInspectorHook(uninstallRuntimeHook);
1047
- await this.inspectorClient?.close();
1107
+ await this.closeInspectorClient();
1108
+ this.stopTunnel();
1109
+ return uninstalled;
1110
+ }
1111
+ async closeInspectorClient() {
1112
+ const client = this.inspectorClient;
1048
1113
  this.inspectorClient = void 0;
1049
- this.tunnelHandle?.stop();
1114
+ try {
1115
+ await client?.close();
1116
+ } catch (error) {
1117
+ this.log(`Live Trace inspector close failed for ${this.options.target.app}: ${formatError(error)}`);
1118
+ }
1119
+ }
1120
+ stopTunnel() {
1121
+ const tunnel = this.tunnelHandle;
1050
1122
  this.tunnelHandle = void 0;
1051
- return uninstalled;
1123
+ try {
1124
+ tunnel?.stop();
1125
+ } catch (error) {
1126
+ this.log(`Live Trace tunnel cleanup failed for ${this.options.target.app}: ${formatError(error)}`);
1127
+ }
1052
1128
  }
1053
1129
  async stopInspectorHook(uninstallRuntimeHook) {
1054
1130
  if (this.inspectorClient === void 0) {
@@ -1092,6 +1168,21 @@ var LiveTraceSession = class {
1092
1168
  log(message) {
1093
1169
  this.options.onLog?.(message);
1094
1170
  }
1171
+ reportInspectorStartup(result) {
1172
+ const normalized = normalizeInspectorStartupResult(result);
1173
+ if (normalized.status === "ready") {
1174
+ return;
1175
+ }
1176
+ const detail = normalized.detail === void 0 ? "" : `: ${normalized.detail}`;
1177
+ this.log(`Node Inspector startup was not confirmed for ${this.options.target.app}${detail}`);
1178
+ }
1179
+ };
1180
+ var LiveTraceStartupError = class extends Error {
1181
+ constructor(message, stateMessage, cause) {
1182
+ super(message, cause === void 0 ? void 0 : { cause });
1183
+ this.stateMessage = stateMessage;
1184
+ }
1185
+ stateMessage;
1095
1186
  };
1096
1187
  function resolveStartOptions(options) {
1097
1188
  return {
@@ -1114,6 +1205,19 @@ function isEvaluateTimeout(error) {
1114
1205
  const message = error instanceof Error ? error.message : String(error);
1115
1206
  return message.includes("Runtime.evaluate timed out");
1116
1207
  }
1208
+ function toStartupError(error) {
1209
+ if (error instanceof LiveTraceStartupError) {
1210
+ return error;
1211
+ }
1212
+ return new LiveTraceStartupError(
1213
+ "Runtime HTTP trace could not be started.",
1214
+ "Runtime HTTP trace could not be started.",
1215
+ error
1216
+ );
1217
+ }
1218
+ function normalizeInspectorStartupResult(result) {
1219
+ return typeof result === "boolean" ? { status: result ? "ready" : "not-ready" } : result;
1220
+ }
1117
1221
  function formatError(error) {
1118
1222
  const message = error instanceof Error ? error.message : String(error);
1119
1223
  return message.trim().length > 0 ? message.trim() : "Unknown error";
@@ -1305,30 +1409,35 @@ async function main(argv) {
1305
1409
  }
1306
1410
  async function runTraceCommand(options) {
1307
1411
  const cfHome = await resolveCfHome(options);
1308
- const events = [];
1309
- const eventLimit = createEventLimit(options);
1310
- const session = new LiveTraceSession({
1311
- target: { ...options.target, cfHomeDir: cfHome.path },
1312
- onState: (event) => {
1313
- if (!options.quiet) {
1314
- writeProgress(event);
1315
- }
1316
- },
1317
- onLog: (message) => {
1318
- if (!options.quiet) {
1319
- writeLog(message);
1412
+ try {
1413
+ const events = [];
1414
+ const eventLimit = createEventLimit(options);
1415
+ const runtimeError = createRuntimeErrorStopWaiter();
1416
+ const session = new LiveTraceSession({
1417
+ target: { ...options.target, cfHomeDir: cfHome.path },
1418
+ onState: (event) => {
1419
+ if (!options.quiet) {
1420
+ writeProgress(event);
1421
+ }
1422
+ runtimeError.report(event);
1423
+ },
1424
+ onLog: (message) => {
1425
+ if (!options.quiet) {
1426
+ writeLog(message);
1427
+ }
1428
+ },
1429
+ onEvents: (batch) => {
1430
+ handleEvents(batch, options, events);
1431
+ eventLimit.check(events.length);
1320
1432
  }
1321
- },
1322
- onEvents: (batch) => {
1323
- handleEvents(batch, options, events);
1324
- eventLimit.check(events.length);
1433
+ });
1434
+ await runUntilStopped(session, options, eventLimit, runtimeError);
1435
+ if (options.format === "json") {
1436
+ writeJson({ events });
1325
1437
  }
1326
- });
1327
- await runUntilStopped(session, options, eventLimit.promise);
1328
- if (options.format === "json") {
1329
- writeJson({ events });
1438
+ } finally {
1439
+ await cfHome.dispose();
1330
1440
  }
1331
- await cfHome.dispose();
1332
1441
  }
1333
1442
  function handleEvents(batch, options, events) {
1334
1443
  for (const event of batch) {
@@ -1341,30 +1450,46 @@ function handleEvents(batch, options, events) {
1341
1450
  }
1342
1451
  }
1343
1452
  }
1344
- async function runUntilStopped(session, options, eventLimit) {
1453
+ async function runUntilStopped(session, options, eventLimit, runtimeError) {
1345
1454
  const abort = createAbortPromise();
1455
+ const duration = createDurationStopWaiter(options.limits.durationMs);
1346
1456
  let stopReason = "user";
1457
+ let failed = false;
1347
1458
  try {
1348
1459
  await session.start(options.trace);
1349
- stopReason = await waitForStop(options, abort.promise, eventLimit);
1460
+ stopReason = await waitForStop([abort, eventLimit, duration, runtimeError]);
1461
+ } catch (error) {
1462
+ failed = true;
1463
+ throw error;
1350
1464
  } finally {
1351
1465
  abort.cleanup();
1352
- await session.stop({ uninstallRuntimeHook: options.uninstallOnExit, reason: stopReason });
1466
+ duration.cleanup();
1467
+ eventLimit.cleanup();
1468
+ runtimeError.cleanup();
1469
+ await session.stop({ uninstallRuntimeHook: options.uninstallOnExit, reason: failed ? "error" : stopReason });
1353
1470
  }
1354
1471
  }
1355
- async function waitForStop(options, abort, eventLimit) {
1356
- const waits = [abort, eventLimit];
1357
- if (options.limits.durationMs !== void 0) {
1358
- waits.push(waitForDuration(options.limits.durationMs));
1359
- }
1360
- return await Promise.race(waits);
1472
+ async function waitForStop(waiters) {
1473
+ return await Promise.race(waiters.map((waiter) => waiter.promise));
1361
1474
  }
1362
- function waitForDuration(durationMs) {
1363
- return new Promise((resolve) => {
1364
- setTimeout(() => {
1475
+ function createDurationStopWaiter(durationMs) {
1476
+ if (durationMs === void 0) {
1477
+ return createNeverStopWaiter();
1478
+ }
1479
+ let timer;
1480
+ const promise = new Promise((resolve) => {
1481
+ timer = setTimeout(() => {
1365
1482
  resolve("duration");
1366
1483
  }, durationMs);
1367
1484
  });
1485
+ return {
1486
+ promise,
1487
+ cleanup: () => {
1488
+ if (timer !== void 0) {
1489
+ clearTimeout(timer);
1490
+ }
1491
+ }
1492
+ };
1368
1493
  }
1369
1494
  function createAbortPromise() {
1370
1495
  let resolveStop = () => {
@@ -1386,12 +1511,50 @@ function createAbortPromise() {
1386
1511
  }
1387
1512
  };
1388
1513
  }
1514
+ function createNeverStopWaiter() {
1515
+ return {
1516
+ promise: new Promise(() => {
1517
+ return;
1518
+ }),
1519
+ cleanup: () => {
1520
+ return;
1521
+ }
1522
+ };
1523
+ }
1524
+ function createRuntimeErrorStopWaiter() {
1525
+ let hasStreamed = false;
1526
+ let settled = false;
1527
+ let rejectStop = () => {
1528
+ return;
1529
+ };
1530
+ const promise = new Promise((_resolve, reject) => {
1531
+ rejectStop = reject;
1532
+ });
1533
+ promise.catch(() => {
1534
+ return;
1535
+ });
1536
+ return {
1537
+ promise,
1538
+ report: (event) => {
1539
+ if (event.state === "streaming") {
1540
+ hasStreamed = true;
1541
+ return;
1542
+ }
1543
+ if (event.state !== "error" || !hasStreamed || settled) {
1544
+ return;
1545
+ }
1546
+ settled = true;
1547
+ rejectStop(new Error(event.message));
1548
+ },
1549
+ cleanup: () => {
1550
+ settled = true;
1551
+ }
1552
+ };
1553
+ }
1389
1554
  function createEventLimit(options) {
1390
1555
  if (options.limits.maxEvents === void 0) {
1391
1556
  return {
1392
- promise: new Promise(() => {
1393
- return;
1394
- }),
1557
+ ...createNeverStopWaiter(),
1395
1558
  check: () => {
1396
1559
  return;
1397
1560
  }
@@ -1409,6 +1572,9 @@ function createEventLimit(options) {
1409
1572
  if (options.limits.maxEvents !== void 0 && count >= options.limits.maxEvents) {
1410
1573
  resolveLimit("max-events");
1411
1574
  }
1575
+ },
1576
+ cleanup: () => {
1577
+ return;
1412
1578
  }
1413
1579
  };
1414
1580
  }
@@ -1429,5 +1595,15 @@ async function resolveCfHome(options) {
1429
1595
  }
1430
1596
 
1431
1597
  // src/cli.ts
1432
- await main(process.argv);
1598
+ try {
1599
+ await main(process4.argv);
1600
+ } catch (error) {
1601
+ process4.stderr.write(`[cf-live-trace] error: ${formatCliError(error)}
1602
+ `);
1603
+ process4.exitCode = 1;
1604
+ }
1605
+ function formatCliError(error) {
1606
+ const message = error instanceof Error ? error.message : String(error);
1607
+ return message.trim().length > 0 ? message.trim() : "Unknown error";
1608
+ }
1433
1609
  //# sourceMappingURL=cli.js.map