@saptools/cf-live-trace 0.1.3 → 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/index.js CHANGED
@@ -461,15 +461,19 @@ async function ensureSshEnabled(target, dependencies = defaultCfDependencies) {
461
461
  });
462
462
  }
463
463
  async function tryStartNodeInspector(target, dependencies = defaultCfDependencies) {
464
+ const result = await startNodeInspector(target, dependencies);
465
+ return result.status === "ready";
466
+ }
467
+ async function startNodeInspector(target, dependencies = defaultCfDependencies) {
468
+ const redactor = createSecretRedactor([target.email, target.password]);
464
469
  try {
465
- const redactor = createSecretRedactor([target.email, target.password]);
466
470
  const stdout = await dependencies.runCf(
467
471
  buildCfSshArgs(target.app, target.instanceIndex, ["-c", buildInspectorSignalCommand()]),
468
472
  { ...buildRunOptions(target, redactor), timeoutMs: INSPECTOR_SIGNAL_TIMEOUT_MS }
469
473
  );
470
- return hasInspectorReadyMarker(stdout);
471
- } catch {
472
- return false;
474
+ return hasInspectorReadyMarker(stdout) ? { status: "ready" } : { status: "not-ready", ...optionalDetail(summarizeInspectorStartupOutput(stdout)) };
475
+ } catch (error) {
476
+ return { status: "not-ready", ...optionalDetail(redactor(formatErrorMessage(error))) };
473
477
  }
474
478
  }
475
479
  async function openInspectorTunnel(target, dependencies = defaultTunnelDependencies) {
@@ -612,6 +616,7 @@ function resolveTunnelAppName(target) {
612
616
  return appName;
613
617
  }
614
618
  async function raceForwardReadiness(handle, dependencies) {
619
+ const controller = new AbortController();
615
620
  let markFailed = () => {
616
621
  return;
617
622
  };
@@ -622,11 +627,14 @@ async function raceForwardReadiness(handle, dependencies) {
622
627
  handle.process.once("exit", markFailed);
623
628
  handle.process.once("error", markFailed);
624
629
  });
625
- const ready = dependencies.waitForLocalPort(handle.localPort, TUNNEL_READY_TIMEOUT_MS);
626
- const outcome = await Promise.race([ready, failedEarly]);
627
- handle.process.removeListener("exit", markFailed);
628
- handle.process.removeListener("error", markFailed);
629
- return outcome;
630
+ const ready = dependencies.waitForLocalPort(handle.localPort, TUNNEL_READY_TIMEOUT_MS, controller.signal);
631
+ try {
632
+ return await Promise.race([ready, failedEarly]);
633
+ } finally {
634
+ controller.abort();
635
+ handle.process.removeListener("exit", markFailed);
636
+ handle.process.removeListener("error", markFailed);
637
+ }
630
638
  }
631
639
  function resolveCommand(command) {
632
640
  const resolvedBin = command ?? process.env["CF_LIVE_TRACE_CF_BIN"] ?? "cf";
@@ -656,12 +664,23 @@ function extractErrorDetail(error) {
656
664
  }
657
665
  return error["message"] instanceof Error ? error["message"].message : "";
658
666
  }
667
+ function formatErrorMessage(error) {
668
+ const message = error instanceof Error ? error.message : String(error);
669
+ return message.trim().length > 0 ? message.trim() : "Unknown error";
670
+ }
659
671
  function formatArgs(args) {
660
672
  return args.join(" ");
661
673
  }
662
674
  function hasInspectorReadyMarker(stdout) {
663
675
  return stdout.split(/\r?\n/).map((line) => line.trim()).includes("saptools-inspector-ready");
664
676
  }
677
+ function summarizeInspectorStartupOutput(stdout) {
678
+ const markers = stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("saptools-inspector-"));
679
+ return markers.length === 0 ? void 0 : markers.join("; ");
680
+ }
681
+ function optionalDetail(detail) {
682
+ return detail === void 0 || detail.length === 0 ? {} : { detail };
683
+ }
665
684
  function findFreePort() {
666
685
  return new Promise((resolve, reject) => {
667
686
  const server = createServer();
@@ -679,29 +698,61 @@ function findFreePort() {
679
698
  });
680
699
  });
681
700
  }
682
- function waitForLocalPort(port, timeoutMs) {
701
+ function waitForLocalPort(port, timeoutMs, signal) {
683
702
  const deadline = Date.now() + timeoutMs;
684
703
  return new Promise((resolve) => {
704
+ let activeSocket;
705
+ let settled = false;
706
+ let retryTimer;
707
+ const finish = (ready) => {
708
+ if (settled) {
709
+ return;
710
+ }
711
+ settled = true;
712
+ if (retryTimer !== void 0) {
713
+ clearTimeout(retryTimer);
714
+ }
715
+ activeSocket?.destroy();
716
+ activeSocket = void 0;
717
+ signal?.removeEventListener("abort", onAbort);
718
+ resolve(ready);
719
+ };
720
+ const onAbort = () => {
721
+ finish(false);
722
+ };
685
723
  const attempt = () => {
724
+ if (settled) {
725
+ return;
726
+ }
686
727
  const socket = netConnect({ host: "127.0.0.1", port });
728
+ activeSocket = socket;
687
729
  socket.once("connect", () => {
688
730
  socket.destroy();
689
- resolve(true);
731
+ activeSocket = void 0;
732
+ finish(true);
690
733
  });
691
734
  socket.once("error", () => {
692
- retryPortProbe(socket, deadline, attempt, resolve);
735
+ activeSocket = void 0;
736
+ retryPortProbe(socket, deadline, attempt, finish, (timer) => {
737
+ retryTimer = timer;
738
+ });
693
739
  });
694
740
  };
741
+ if (signal?.aborted === true) {
742
+ finish(false);
743
+ return;
744
+ }
745
+ signal?.addEventListener("abort", onAbort, { once: true });
695
746
  attempt();
696
747
  });
697
748
  }
698
- function retryPortProbe(socket, deadline, attempt, resolve) {
749
+ function retryPortProbe(socket, deadline, attempt, finish, setRetryTimer) {
699
750
  socket.destroy();
700
751
  if (Date.now() >= deadline) {
701
- resolve(false);
752
+ finish(false);
702
753
  return;
703
754
  }
704
- setTimeout(attempt, TUNNEL_READY_POLL_MS);
755
+ setRetryTimer(setTimeout(attempt, TUNNEL_READY_POLL_MS));
705
756
  }
706
757
  function isRecord2(value) {
707
758
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -816,7 +867,7 @@ var DRAIN_TRANSPORT_BODY_LIMIT = 2e4;
816
867
  var defaultDependencies = {
817
868
  prepareCfSession,
818
869
  ensureSshEnabled,
819
- tryStartNodeInspector,
870
+ tryStartNodeInspector: startNodeInspector,
820
871
  openInspectorTunnel,
821
872
  connectInspector: connectRuntimeInspector,
822
873
  setInterval,
@@ -869,9 +920,11 @@ var LiveTraceSession = class {
869
920
  } catch (error) {
870
921
  this.log(`Live Trace startup failed for ${this.options.target.app}: ${formatError(error)}`);
871
922
  await this.stopRuntimeTrace(false);
923
+ const startupError = toStartupError(error);
872
924
  if (!this.shouldStop()) {
873
- this.postState("error", "Runtime HTTP trace could not be started.", false, false);
925
+ this.postState("error", startupError.stateMessage, false, false);
874
926
  }
927
+ throw startupError;
875
928
  }
876
929
  }
877
930
  async startInspector(options) {
@@ -881,7 +934,7 @@ var LiveTraceSession = class {
881
934
  return;
882
935
  }
883
936
  this.postState("starting-inspector", "Requesting Node Inspector startup.", false, false);
884
- await this.dependencies.tryStartNodeInspector(this.options.target);
937
+ this.reportInspectorStartup(await this.dependencies.tryStartNodeInspector(this.options.target));
885
938
  if (this.shouldStop()) {
886
939
  return;
887
940
  }
@@ -895,8 +948,10 @@ var LiveTraceSession = class {
895
948
  return;
896
949
  }
897
950
  if (tunnel.status !== "ready") {
898
- this.postState("error", "Node Inspector is not reachable on 127.0.0.1:9229.", false, false);
899
- return;
951
+ throw new LiveTraceStartupError(
952
+ "Node Inspector is not reachable on 127.0.0.1:9229.",
953
+ "Node Inspector is not reachable on 127.0.0.1:9229."
954
+ );
900
955
  }
901
956
  this.tunnelHandle = tunnel.handle;
902
957
  this.inspectorClient = await this.dependencies.connectInspector(tunnel.handle.localPort);
@@ -967,11 +1022,27 @@ var LiveTraceSession = class {
967
1022
  this.stopPolling();
968
1023
  this.consecutiveDrainTimeouts = 0;
969
1024
  const uninstalled = await this.stopInspectorHook(uninstallRuntimeHook);
970
- await this.inspectorClient?.close();
1025
+ await this.closeInspectorClient();
1026
+ this.stopTunnel();
1027
+ return uninstalled;
1028
+ }
1029
+ async closeInspectorClient() {
1030
+ const client = this.inspectorClient;
971
1031
  this.inspectorClient = void 0;
972
- this.tunnelHandle?.stop();
1032
+ try {
1033
+ await client?.close();
1034
+ } catch (error) {
1035
+ this.log(`Live Trace inspector close failed for ${this.options.target.app}: ${formatError(error)}`);
1036
+ }
1037
+ }
1038
+ stopTunnel() {
1039
+ const tunnel = this.tunnelHandle;
973
1040
  this.tunnelHandle = void 0;
974
- return uninstalled;
1041
+ try {
1042
+ tunnel?.stop();
1043
+ } catch (error) {
1044
+ this.log(`Live Trace tunnel cleanup failed for ${this.options.target.app}: ${formatError(error)}`);
1045
+ }
975
1046
  }
976
1047
  async stopInspectorHook(uninstallRuntimeHook) {
977
1048
  if (this.inspectorClient === void 0) {
@@ -1015,6 +1086,21 @@ var LiveTraceSession = class {
1015
1086
  log(message) {
1016
1087
  this.options.onLog?.(message);
1017
1088
  }
1089
+ reportInspectorStartup(result) {
1090
+ const normalized = normalizeInspectorStartupResult(result);
1091
+ if (normalized.status === "ready") {
1092
+ return;
1093
+ }
1094
+ const detail = normalized.detail === void 0 ? "" : `: ${normalized.detail}`;
1095
+ this.log(`Node Inspector startup was not confirmed for ${this.options.target.app}${detail}`);
1096
+ }
1097
+ };
1098
+ var LiveTraceStartupError = class extends Error {
1099
+ constructor(message, stateMessage, cause) {
1100
+ super(message, cause === void 0 ? void 0 : { cause });
1101
+ this.stateMessage = stateMessage;
1102
+ }
1103
+ stateMessage;
1018
1104
  };
1019
1105
  function resolveStartOptions(options) {
1020
1106
  return {
@@ -1037,6 +1123,19 @@ function isEvaluateTimeout(error) {
1037
1123
  const message = error instanceof Error ? error.message : String(error);
1038
1124
  return message.includes("Runtime.evaluate timed out");
1039
1125
  }
1126
+ function toStartupError(error) {
1127
+ if (error instanceof LiveTraceStartupError) {
1128
+ return error;
1129
+ }
1130
+ return new LiveTraceStartupError(
1131
+ "Runtime HTTP trace could not be started.",
1132
+ "Runtime HTTP trace could not be started.",
1133
+ error
1134
+ );
1135
+ }
1136
+ function normalizeInspectorStartupResult(result) {
1137
+ return typeof result === "boolean" ? { status: result ? "ready" : "not-ready" } : result;
1138
+ }
1040
1139
  function formatError(error) {
1041
1140
  const message = error instanceof Error ? error.message : String(error);
1042
1141
  return message.trim().length > 0 ? message.trim() : "Unknown error";
@@ -1057,6 +1156,7 @@ export {
1057
1156
  openInspectorTunnel,
1058
1157
  parseDrainResult,
1059
1158
  prepareCfSession,
1159
+ startNodeInspector,
1060
1160
  truncatePreview,
1061
1161
  tryStartNodeInspector
1062
1162
  };