@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/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/runtime-source.ts
2
2
  var CF_LIVE_TRACE_GLOBAL_NAME = "__SAPTOOLS_CF_LIVE_TRACE__";
3
- var CF_LIVE_TRACE_RUNTIME_VERSION = 1;
3
+ var CF_LIVE_TRACE_RUNTIME_VERSION = 2;
4
4
  var CF_LIVE_TRACE_RUNTIME_SOURCE = `
5
5
  (() => {
6
6
  const name = '${CF_LIVE_TRACE_GLOBAL_NAME}';
@@ -197,15 +197,12 @@ var CF_LIVE_TRACE_RUNTIME_SOURCE = `
197
197
  status() {
198
198
  return { installed: state.installed, enabled: state.enabled, queueSize: state.queue.length, droppedCount: state.droppedCount, maxEvents: state.options.maxEvents };
199
199
  },
200
- uninstall() {
200
+ async uninstall() {
201
201
  state.enabled = false;
202
- const requireFn = loadRequire();
203
- if (requireFn) {
204
- const http = requireFn('http');
205
- const https = requireFn('https');
206
- if (state.originals.httpServerEmit && http && http.Server) http.Server.prototype.emit = state.originals.httpServerEmit;
207
- if (state.originals.httpsServerEmit && https && https.Server) https.Server.prototype.emit = state.originals.httpsServerEmit;
208
- }
202
+ const http = await loadModule('http');
203
+ const https = await loadModule('https');
204
+ if (state.originals.httpServerEmit && http && http.Server) http.Server.prototype.emit = state.originals.httpServerEmit;
205
+ if (state.originals.httpsServerEmit && https && https.Server) https.Server.prototype.emit = state.originals.httpsServerEmit;
209
206
  state.installed = false;
210
207
  return api.status();
211
208
  }
@@ -461,15 +458,19 @@ async function ensureSshEnabled(target, dependencies = defaultCfDependencies) {
461
458
  });
462
459
  }
463
460
  async function tryStartNodeInspector(target, dependencies = defaultCfDependencies) {
461
+ const result = await startNodeInspector(target, dependencies);
462
+ return result.status === "ready";
463
+ }
464
+ async function startNodeInspector(target, dependencies = defaultCfDependencies) {
465
+ const redactor = createSecretRedactor([target.email, target.password]);
464
466
  try {
465
- const redactor = createSecretRedactor([target.email, target.password]);
466
467
  const stdout = await dependencies.runCf(
467
468
  buildCfSshArgs(target.app, target.instanceIndex, ["-c", buildInspectorSignalCommand()]),
468
469
  { ...buildRunOptions(target, redactor), timeoutMs: INSPECTOR_SIGNAL_TIMEOUT_MS }
469
470
  );
470
- return hasInspectorReadyMarker(stdout);
471
- } catch {
472
- return false;
471
+ return hasInspectorReadyMarker(stdout) ? { status: "ready" } : { status: "not-ready", ...optionalDetail(summarizeInspectorStartupOutput(stdout)) };
472
+ } catch (error) {
473
+ return { status: "not-ready", ...optionalDetail(redactor(formatErrorMessage(error))) };
473
474
  }
474
475
  }
475
476
  async function openInspectorTunnel(target, dependencies = defaultTunnelDependencies) {
@@ -517,6 +518,8 @@ function spawnPortForward(params) {
517
518
  env: buildCfEnv(params.cfHomeDir),
518
519
  stdio: ["ignore", "pipe", "pipe"]
519
520
  });
521
+ child.stdout.resume();
522
+ child.stderr.resume();
520
523
  return {
521
524
  process: child,
522
525
  localPort: params.localPort,
@@ -612,6 +615,7 @@ function resolveTunnelAppName(target) {
612
615
  return appName;
613
616
  }
614
617
  async function raceForwardReadiness(handle, dependencies) {
618
+ const controller = new AbortController();
615
619
  let markFailed = () => {
616
620
  return;
617
621
  };
@@ -622,11 +626,14 @@ async function raceForwardReadiness(handle, dependencies) {
622
626
  handle.process.once("exit", markFailed);
623
627
  handle.process.once("error", markFailed);
624
628
  });
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;
629
+ const ready = dependencies.waitForLocalPort(handle.localPort, TUNNEL_READY_TIMEOUT_MS, controller.signal);
630
+ try {
631
+ return await Promise.race([ready, failedEarly]);
632
+ } finally {
633
+ controller.abort();
634
+ handle.process.removeListener("exit", markFailed);
635
+ handle.process.removeListener("error", markFailed);
636
+ }
630
637
  }
631
638
  function resolveCommand(command) {
632
639
  const resolvedBin = command ?? process.env["CF_LIVE_TRACE_CF_BIN"] ?? "cf";
@@ -654,7 +661,15 @@ function extractErrorDetail(error) {
654
661
  if (stderr.length > 0) {
655
662
  return stderr;
656
663
  }
657
- return error["message"] instanceof Error ? error["message"].message : "";
664
+ const message = error["message"];
665
+ if (typeof message === "string") {
666
+ return message.trim();
667
+ }
668
+ return message instanceof Error ? message.message.trim() : "";
669
+ }
670
+ function formatErrorMessage(error) {
671
+ const message = error instanceof Error ? error.message : String(error);
672
+ return message.trim().length > 0 ? message.trim() : "Unknown error";
658
673
  }
659
674
  function formatArgs(args) {
660
675
  return args.join(" ");
@@ -662,6 +677,13 @@ function formatArgs(args) {
662
677
  function hasInspectorReadyMarker(stdout) {
663
678
  return stdout.split(/\r?\n/).map((line) => line.trim()).includes("saptools-inspector-ready");
664
679
  }
680
+ function summarizeInspectorStartupOutput(stdout) {
681
+ const markers = stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("saptools-inspector-"));
682
+ return markers.length === 0 ? void 0 : markers.join("; ");
683
+ }
684
+ function optionalDetail(detail) {
685
+ return detail === void 0 || detail.length === 0 ? {} : { detail };
686
+ }
665
687
  function findFreePort() {
666
688
  return new Promise((resolve, reject) => {
667
689
  const server = createServer();
@@ -679,29 +701,61 @@ function findFreePort() {
679
701
  });
680
702
  });
681
703
  }
682
- function waitForLocalPort(port, timeoutMs) {
704
+ function waitForLocalPort(port, timeoutMs, signal) {
683
705
  const deadline = Date.now() + timeoutMs;
684
706
  return new Promise((resolve) => {
707
+ let activeSocket;
708
+ let settled = false;
709
+ let retryTimer;
710
+ const finish = (ready) => {
711
+ if (settled) {
712
+ return;
713
+ }
714
+ settled = true;
715
+ if (retryTimer !== void 0) {
716
+ clearTimeout(retryTimer);
717
+ }
718
+ activeSocket?.destroy();
719
+ activeSocket = void 0;
720
+ signal?.removeEventListener("abort", onAbort);
721
+ resolve(ready);
722
+ };
723
+ const onAbort = () => {
724
+ finish(false);
725
+ };
685
726
  const attempt = () => {
727
+ if (settled) {
728
+ return;
729
+ }
686
730
  const socket = netConnect({ host: "127.0.0.1", port });
731
+ activeSocket = socket;
687
732
  socket.once("connect", () => {
688
733
  socket.destroy();
689
- resolve(true);
734
+ activeSocket = void 0;
735
+ finish(true);
690
736
  });
691
737
  socket.once("error", () => {
692
- retryPortProbe(socket, deadline, attempt, resolve);
738
+ activeSocket = void 0;
739
+ retryPortProbe(socket, deadline, attempt, finish, (timer) => {
740
+ retryTimer = timer;
741
+ });
693
742
  });
694
743
  };
744
+ if (signal?.aborted === true) {
745
+ finish(false);
746
+ return;
747
+ }
748
+ signal?.addEventListener("abort", onAbort, { once: true });
695
749
  attempt();
696
750
  });
697
751
  }
698
- function retryPortProbe(socket, deadline, attempt, resolve) {
752
+ function retryPortProbe(socket, deadline, attempt, finish, setRetryTimer) {
699
753
  socket.destroy();
700
754
  if (Date.now() >= deadline) {
701
- resolve(false);
755
+ finish(false);
702
756
  return;
703
757
  }
704
- setTimeout(attempt, TUNNEL_READY_POLL_MS);
758
+ setRetryTimer(setTimeout(attempt, TUNNEL_READY_POLL_MS));
705
759
  }
706
760
  function isRecord2(value) {
707
761
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -816,7 +870,7 @@ var DRAIN_TRANSPORT_BODY_LIMIT = 2e4;
816
870
  var defaultDependencies = {
817
871
  prepareCfSession,
818
872
  ensureSshEnabled,
819
- tryStartNodeInspector,
873
+ tryStartNodeInspector: startNodeInspector,
820
874
  openInspectorTunnel,
821
875
  connectInspector: connectRuntimeInspector,
822
876
  setInterval,
@@ -847,6 +901,9 @@ var LiveTraceSession = class {
847
901
  async stop(options) {
848
902
  this.stopRequested = true;
849
903
  if (!this.isRunning()) {
904
+ if (this.state === "error" && options.reason === "error") {
905
+ return;
906
+ }
850
907
  this.postState("stopped", `Trace stopped (${options.reason}).`, false, false);
851
908
  return;
852
909
  }
@@ -869,9 +926,11 @@ var LiveTraceSession = class {
869
926
  } catch (error) {
870
927
  this.log(`Live Trace startup failed for ${this.options.target.app}: ${formatError(error)}`);
871
928
  await this.stopRuntimeTrace(false);
929
+ const startupError = toStartupError(error);
872
930
  if (!this.shouldStop()) {
873
- this.postState("error", "Runtime HTTP trace could not be started.", false, false);
931
+ this.postState("error", startupError.stateMessage, false, false);
874
932
  }
933
+ throw startupError;
875
934
  }
876
935
  }
877
936
  async startInspector(options) {
@@ -881,7 +940,7 @@ var LiveTraceSession = class {
881
940
  return;
882
941
  }
883
942
  this.postState("starting-inspector", "Requesting Node Inspector startup.", false, false);
884
- await this.dependencies.tryStartNodeInspector(this.options.target);
943
+ this.reportInspectorStartup(await this.dependencies.tryStartNodeInspector(this.options.target));
885
944
  if (this.shouldStop()) {
886
945
  return;
887
946
  }
@@ -895,8 +954,10 @@ var LiveTraceSession = class {
895
954
  return;
896
955
  }
897
956
  if (tunnel.status !== "ready") {
898
- this.postState("error", "Node Inspector is not reachable on 127.0.0.1:9229.", false, false);
899
- return;
957
+ throw new LiveTraceStartupError(
958
+ "Node Inspector is not reachable on 127.0.0.1:9229.",
959
+ "Node Inspector is not reachable on 127.0.0.1:9229."
960
+ );
900
961
  }
901
962
  this.tunnelHandle = tunnel.handle;
902
963
  this.inspectorClient = await this.dependencies.connectInspector(tunnel.handle.localPort);
@@ -967,11 +1028,27 @@ var LiveTraceSession = class {
967
1028
  this.stopPolling();
968
1029
  this.consecutiveDrainTimeouts = 0;
969
1030
  const uninstalled = await this.stopInspectorHook(uninstallRuntimeHook);
970
- await this.inspectorClient?.close();
1031
+ await this.closeInspectorClient();
1032
+ this.stopTunnel();
1033
+ return uninstalled;
1034
+ }
1035
+ async closeInspectorClient() {
1036
+ const client = this.inspectorClient;
971
1037
  this.inspectorClient = void 0;
972
- this.tunnelHandle?.stop();
1038
+ try {
1039
+ await client?.close();
1040
+ } catch (error) {
1041
+ this.log(`Live Trace inspector close failed for ${this.options.target.app}: ${formatError(error)}`);
1042
+ }
1043
+ }
1044
+ stopTunnel() {
1045
+ const tunnel = this.tunnelHandle;
973
1046
  this.tunnelHandle = void 0;
974
- return uninstalled;
1047
+ try {
1048
+ tunnel?.stop();
1049
+ } catch (error) {
1050
+ this.log(`Live Trace tunnel cleanup failed for ${this.options.target.app}: ${formatError(error)}`);
1051
+ }
975
1052
  }
976
1053
  async stopInspectorHook(uninstallRuntimeHook) {
977
1054
  if (this.inspectorClient === void 0) {
@@ -1015,6 +1092,21 @@ var LiveTraceSession = class {
1015
1092
  log(message) {
1016
1093
  this.options.onLog?.(message);
1017
1094
  }
1095
+ reportInspectorStartup(result) {
1096
+ const normalized = normalizeInspectorStartupResult(result);
1097
+ if (normalized.status === "ready") {
1098
+ return;
1099
+ }
1100
+ const detail = normalized.detail === void 0 ? "" : `: ${normalized.detail}`;
1101
+ this.log(`Node Inspector startup was not confirmed for ${this.options.target.app}${detail}`);
1102
+ }
1103
+ };
1104
+ var LiveTraceStartupError = class extends Error {
1105
+ constructor(message, stateMessage, cause) {
1106
+ super(message, cause === void 0 ? void 0 : { cause });
1107
+ this.stateMessage = stateMessage;
1108
+ }
1109
+ stateMessage;
1018
1110
  };
1019
1111
  function resolveStartOptions(options) {
1020
1112
  return {
@@ -1037,6 +1129,19 @@ function isEvaluateTimeout(error) {
1037
1129
  const message = error instanceof Error ? error.message : String(error);
1038
1130
  return message.includes("Runtime.evaluate timed out");
1039
1131
  }
1132
+ function toStartupError(error) {
1133
+ if (error instanceof LiveTraceStartupError) {
1134
+ return error;
1135
+ }
1136
+ return new LiveTraceStartupError(
1137
+ "Runtime HTTP trace could not be started.",
1138
+ "Runtime HTTP trace could not be started.",
1139
+ error
1140
+ );
1141
+ }
1142
+ function normalizeInspectorStartupResult(result) {
1143
+ return typeof result === "boolean" ? { status: result ? "ready" : "not-ready" } : result;
1144
+ }
1040
1145
  function formatError(error) {
1041
1146
  const message = error instanceof Error ? error.message : String(error);
1042
1147
  return message.trim().length > 0 ? message.trim() : "Unknown error";
@@ -1057,6 +1162,7 @@ export {
1057
1162
  openInspectorTunnel,
1058
1163
  parseDrainResult,
1059
1164
  prepareCfSession,
1165
+ startNodeInspector,
1060
1166
  truncatePreview,
1061
1167
  tryStartNodeInspector
1062
1168
  };