@saptools/cf-live-trace 0.1.4 → 0.1.5

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) {
@@ -216,6 +219,7 @@ function resolveTunnelAppName(target) {
216
219
  return appName;
217
220
  }
218
221
  async function raceForwardReadiness(handle, dependencies) {
222
+ const controller = new AbortController();
219
223
  let markFailed = () => {
220
224
  return;
221
225
  };
@@ -226,11 +230,14 @@ async function raceForwardReadiness(handle, dependencies) {
226
230
  handle.process.once("exit", markFailed);
227
231
  handle.process.once("error", markFailed);
228
232
  });
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;
233
+ const ready = dependencies.waitForLocalPort(handle.localPort, TUNNEL_READY_TIMEOUT_MS, controller.signal);
234
+ try {
235
+ return await Promise.race([ready, failedEarly]);
236
+ } finally {
237
+ controller.abort();
238
+ handle.process.removeListener("exit", markFailed);
239
+ handle.process.removeListener("error", markFailed);
240
+ }
234
241
  }
235
242
  function resolveCommand(command) {
236
243
  const resolvedBin = command ?? process.env["CF_LIVE_TRACE_CF_BIN"] ?? "cf";
@@ -260,12 +267,23 @@ function extractErrorDetail(error) {
260
267
  }
261
268
  return error["message"] instanceof Error ? error["message"].message : "";
262
269
  }
270
+ function formatErrorMessage(error) {
271
+ const message = error instanceof Error ? error.message : String(error);
272
+ return message.trim().length > 0 ? message.trim() : "Unknown error";
273
+ }
263
274
  function formatArgs(args) {
264
275
  return args.join(" ");
265
276
  }
266
277
  function hasInspectorReadyMarker(stdout) {
267
278
  return stdout.split(/\r?\n/).map((line) => line.trim()).includes("saptools-inspector-ready");
268
279
  }
280
+ function summarizeInspectorStartupOutput(stdout) {
281
+ const markers = stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("saptools-inspector-"));
282
+ return markers.length === 0 ? void 0 : markers.join("; ");
283
+ }
284
+ function optionalDetail(detail) {
285
+ return detail === void 0 || detail.length === 0 ? {} : { detail };
286
+ }
269
287
  function findFreePort() {
270
288
  return new Promise((resolve, reject) => {
271
289
  const server = createServer();
@@ -283,29 +301,61 @@ function findFreePort() {
283
301
  });
284
302
  });
285
303
  }
286
- function waitForLocalPort(port, timeoutMs) {
304
+ function waitForLocalPort(port, timeoutMs, signal) {
287
305
  const deadline = Date.now() + timeoutMs;
288
306
  return new Promise((resolve) => {
307
+ let activeSocket;
308
+ let settled = false;
309
+ let retryTimer;
310
+ const finish = (ready) => {
311
+ if (settled) {
312
+ return;
313
+ }
314
+ settled = true;
315
+ if (retryTimer !== void 0) {
316
+ clearTimeout(retryTimer);
317
+ }
318
+ activeSocket?.destroy();
319
+ activeSocket = void 0;
320
+ signal?.removeEventListener("abort", onAbort);
321
+ resolve(ready);
322
+ };
323
+ const onAbort = () => {
324
+ finish(false);
325
+ };
289
326
  const attempt = () => {
327
+ if (settled) {
328
+ return;
329
+ }
290
330
  const socket = netConnect({ host: "127.0.0.1", port });
331
+ activeSocket = socket;
291
332
  socket.once("connect", () => {
292
333
  socket.destroy();
293
- resolve(true);
334
+ activeSocket = void 0;
335
+ finish(true);
294
336
  });
295
337
  socket.once("error", () => {
296
- retryPortProbe(socket, deadline, attempt, resolve);
338
+ activeSocket = void 0;
339
+ retryPortProbe(socket, deadline, attempt, finish, (timer) => {
340
+ retryTimer = timer;
341
+ });
297
342
  });
298
343
  };
344
+ if (signal?.aborted === true) {
345
+ finish(false);
346
+ return;
347
+ }
348
+ signal?.addEventListener("abort", onAbort, { once: true });
299
349
  attempt();
300
350
  });
301
351
  }
302
- function retryPortProbe(socket, deadline, attempt, resolve) {
352
+ function retryPortProbe(socket, deadline, attempt, finish, setRetryTimer) {
303
353
  socket.destroy();
304
354
  if (Date.now() >= deadline) {
305
- resolve(false);
355
+ finish(false);
306
356
  return;
307
357
  }
308
- setTimeout(attempt, TUNNEL_READY_POLL_MS);
358
+ setRetryTimer(setTimeout(attempt, TUNNEL_READY_POLL_MS));
309
359
  }
310
360
  function isRecord(value) {
311
361
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -893,7 +943,7 @@ var DRAIN_TRANSPORT_BODY_LIMIT = 2e4;
893
943
  var defaultDependencies = {
894
944
  prepareCfSession,
895
945
  ensureSshEnabled,
896
- tryStartNodeInspector,
946
+ tryStartNodeInspector: startNodeInspector,
897
947
  openInspectorTunnel,
898
948
  connectInspector: connectRuntimeInspector,
899
949
  setInterval,
@@ -946,9 +996,11 @@ var LiveTraceSession = class {
946
996
  } catch (error) {
947
997
  this.log(`Live Trace startup failed for ${this.options.target.app}: ${formatError(error)}`);
948
998
  await this.stopRuntimeTrace(false);
999
+ const startupError = toStartupError(error);
949
1000
  if (!this.shouldStop()) {
950
- this.postState("error", "Runtime HTTP trace could not be started.", false, false);
1001
+ this.postState("error", startupError.stateMessage, false, false);
951
1002
  }
1003
+ throw startupError;
952
1004
  }
953
1005
  }
954
1006
  async startInspector(options) {
@@ -958,7 +1010,7 @@ var LiveTraceSession = class {
958
1010
  return;
959
1011
  }
960
1012
  this.postState("starting-inspector", "Requesting Node Inspector startup.", false, false);
961
- await this.dependencies.tryStartNodeInspector(this.options.target);
1013
+ this.reportInspectorStartup(await this.dependencies.tryStartNodeInspector(this.options.target));
962
1014
  if (this.shouldStop()) {
963
1015
  return;
964
1016
  }
@@ -972,8 +1024,10 @@ var LiveTraceSession = class {
972
1024
  return;
973
1025
  }
974
1026
  if (tunnel.status !== "ready") {
975
- this.postState("error", "Node Inspector is not reachable on 127.0.0.1:9229.", false, false);
976
- return;
1027
+ throw new LiveTraceStartupError(
1028
+ "Node Inspector is not reachable on 127.0.0.1:9229.",
1029
+ "Node Inspector is not reachable on 127.0.0.1:9229."
1030
+ );
977
1031
  }
978
1032
  this.tunnelHandle = tunnel.handle;
979
1033
  this.inspectorClient = await this.dependencies.connectInspector(tunnel.handle.localPort);
@@ -1044,11 +1098,27 @@ var LiveTraceSession = class {
1044
1098
  this.stopPolling();
1045
1099
  this.consecutiveDrainTimeouts = 0;
1046
1100
  const uninstalled = await this.stopInspectorHook(uninstallRuntimeHook);
1047
- await this.inspectorClient?.close();
1101
+ await this.closeInspectorClient();
1102
+ this.stopTunnel();
1103
+ return uninstalled;
1104
+ }
1105
+ async closeInspectorClient() {
1106
+ const client = this.inspectorClient;
1048
1107
  this.inspectorClient = void 0;
1049
- this.tunnelHandle?.stop();
1108
+ try {
1109
+ await client?.close();
1110
+ } catch (error) {
1111
+ this.log(`Live Trace inspector close failed for ${this.options.target.app}: ${formatError(error)}`);
1112
+ }
1113
+ }
1114
+ stopTunnel() {
1115
+ const tunnel = this.tunnelHandle;
1050
1116
  this.tunnelHandle = void 0;
1051
- return uninstalled;
1117
+ try {
1118
+ tunnel?.stop();
1119
+ } catch (error) {
1120
+ this.log(`Live Trace tunnel cleanup failed for ${this.options.target.app}: ${formatError(error)}`);
1121
+ }
1052
1122
  }
1053
1123
  async stopInspectorHook(uninstallRuntimeHook) {
1054
1124
  if (this.inspectorClient === void 0) {
@@ -1092,6 +1162,21 @@ var LiveTraceSession = class {
1092
1162
  log(message) {
1093
1163
  this.options.onLog?.(message);
1094
1164
  }
1165
+ reportInspectorStartup(result) {
1166
+ const normalized = normalizeInspectorStartupResult(result);
1167
+ if (normalized.status === "ready") {
1168
+ return;
1169
+ }
1170
+ const detail = normalized.detail === void 0 ? "" : `: ${normalized.detail}`;
1171
+ this.log(`Node Inspector startup was not confirmed for ${this.options.target.app}${detail}`);
1172
+ }
1173
+ };
1174
+ var LiveTraceStartupError = class extends Error {
1175
+ constructor(message, stateMessage, cause) {
1176
+ super(message, cause === void 0 ? void 0 : { cause });
1177
+ this.stateMessage = stateMessage;
1178
+ }
1179
+ stateMessage;
1095
1180
  };
1096
1181
  function resolveStartOptions(options) {
1097
1182
  return {
@@ -1114,6 +1199,19 @@ function isEvaluateTimeout(error) {
1114
1199
  const message = error instanceof Error ? error.message : String(error);
1115
1200
  return message.includes("Runtime.evaluate timed out");
1116
1201
  }
1202
+ function toStartupError(error) {
1203
+ if (error instanceof LiveTraceStartupError) {
1204
+ return error;
1205
+ }
1206
+ return new LiveTraceStartupError(
1207
+ "Runtime HTTP trace could not be started.",
1208
+ "Runtime HTTP trace could not be started.",
1209
+ error
1210
+ );
1211
+ }
1212
+ function normalizeInspectorStartupResult(result) {
1213
+ return typeof result === "boolean" ? { status: result ? "ready" : "not-ready" } : result;
1214
+ }
1117
1215
  function formatError(error) {
1118
1216
  const message = error instanceof Error ? error.message : String(error);
1119
1217
  return message.trim().length > 0 ? message.trim() : "Unknown error";
@@ -1305,30 +1403,33 @@ async function main(argv) {
1305
1403
  }
1306
1404
  async function runTraceCommand(options) {
1307
1405
  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);
1406
+ try {
1407
+ const events = [];
1408
+ const eventLimit = createEventLimit(options);
1409
+ const session = new LiveTraceSession({
1410
+ target: { ...options.target, cfHomeDir: cfHome.path },
1411
+ onState: (event) => {
1412
+ if (!options.quiet) {
1413
+ writeProgress(event);
1414
+ }
1415
+ },
1416
+ onLog: (message) => {
1417
+ if (!options.quiet) {
1418
+ writeLog(message);
1419
+ }
1420
+ },
1421
+ onEvents: (batch) => {
1422
+ handleEvents(batch, options, events);
1423
+ eventLimit.check(events.length);
1320
1424
  }
1321
- },
1322
- onEvents: (batch) => {
1323
- handleEvents(batch, options, events);
1324
- eventLimit.check(events.length);
1425
+ });
1426
+ await runUntilStopped(session, options, eventLimit);
1427
+ if (options.format === "json") {
1428
+ writeJson({ events });
1325
1429
  }
1326
- });
1327
- await runUntilStopped(session, options, eventLimit.promise);
1328
- if (options.format === "json") {
1329
- writeJson({ events });
1430
+ } finally {
1431
+ await cfHome.dispose();
1330
1432
  }
1331
- await cfHome.dispose();
1332
1433
  }
1333
1434
  function handleEvents(batch, options, events) {
1334
1435
  for (const event of batch) {
@@ -1343,28 +1444,43 @@ function handleEvents(batch, options, events) {
1343
1444
  }
1344
1445
  async function runUntilStopped(session, options, eventLimit) {
1345
1446
  const abort = createAbortPromise();
1447
+ const duration = createDurationStopWaiter(options.limits.durationMs);
1346
1448
  let stopReason = "user";
1449
+ let failed = false;
1347
1450
  try {
1348
1451
  await session.start(options.trace);
1349
- stopReason = await waitForStop(options, abort.promise, eventLimit);
1452
+ stopReason = await waitForStop([abort, eventLimit, duration]);
1453
+ } catch (error) {
1454
+ failed = true;
1455
+ throw error;
1350
1456
  } finally {
1351
1457
  abort.cleanup();
1352
- await session.stop({ uninstallRuntimeHook: options.uninstallOnExit, reason: stopReason });
1458
+ duration.cleanup();
1459
+ eventLimit.cleanup();
1460
+ await session.stop({ uninstallRuntimeHook: options.uninstallOnExit, reason: failed ? "error" : stopReason });
1353
1461
  }
1354
1462
  }
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);
1463
+ async function waitForStop(waiters) {
1464
+ return await Promise.race(waiters.map((waiter) => waiter.promise));
1361
1465
  }
1362
- function waitForDuration(durationMs) {
1363
- return new Promise((resolve) => {
1364
- setTimeout(() => {
1466
+ function createDurationStopWaiter(durationMs) {
1467
+ if (durationMs === void 0) {
1468
+ return createNeverStopWaiter();
1469
+ }
1470
+ let timer;
1471
+ const promise = new Promise((resolve) => {
1472
+ timer = setTimeout(() => {
1365
1473
  resolve("duration");
1366
1474
  }, durationMs);
1367
1475
  });
1476
+ return {
1477
+ promise,
1478
+ cleanup: () => {
1479
+ if (timer !== void 0) {
1480
+ clearTimeout(timer);
1481
+ }
1482
+ }
1483
+ };
1368
1484
  }
1369
1485
  function createAbortPromise() {
1370
1486
  let resolveStop = () => {
@@ -1386,12 +1502,20 @@ function createAbortPromise() {
1386
1502
  }
1387
1503
  };
1388
1504
  }
1505
+ function createNeverStopWaiter() {
1506
+ return {
1507
+ promise: new Promise(() => {
1508
+ return;
1509
+ }),
1510
+ cleanup: () => {
1511
+ return;
1512
+ }
1513
+ };
1514
+ }
1389
1515
  function createEventLimit(options) {
1390
1516
  if (options.limits.maxEvents === void 0) {
1391
1517
  return {
1392
- promise: new Promise(() => {
1393
- return;
1394
- }),
1518
+ ...createNeverStopWaiter(),
1395
1519
  check: () => {
1396
1520
  return;
1397
1521
  }
@@ -1409,6 +1533,9 @@ function createEventLimit(options) {
1409
1533
  if (options.limits.maxEvents !== void 0 && count >= options.limits.maxEvents) {
1410
1534
  resolveLimit("max-events");
1411
1535
  }
1536
+ },
1537
+ cleanup: () => {
1538
+ return;
1412
1539
  }
1413
1540
  };
1414
1541
  }
@@ -1429,5 +1556,15 @@ async function resolveCfHome(options) {
1429
1556
  }
1430
1557
 
1431
1558
  // src/cli.ts
1432
- await main(process.argv);
1559
+ try {
1560
+ await main(process4.argv);
1561
+ } catch (error) {
1562
+ process4.stderr.write(`[cf-live-trace] error: ${formatCliError(error)}
1563
+ `);
1564
+ process4.exitCode = 1;
1565
+ }
1566
+ function formatCliError(error) {
1567
+ const message = error instanceof Error ? error.message : String(error);
1568
+ return message.trim().length > 0 ? message.trim() : "Unknown error";
1569
+ }
1433
1570
  //# sourceMappingURL=cli.js.map