@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/cli.js +197 -60
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +14 -3
- package/dist/index.js +123 -23
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
|
|
731
|
+
activeSocket = void 0;
|
|
732
|
+
finish(true);
|
|
690
733
|
});
|
|
691
734
|
socket.once("error", () => {
|
|
692
|
-
|
|
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,
|
|
749
|
+
function retryPortProbe(socket, deadline, attempt, finish, setRetryTimer) {
|
|
699
750
|
socket.destroy();
|
|
700
751
|
if (Date.now() >= deadline) {
|
|
701
|
-
|
|
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",
|
|
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
|
-
|
|
899
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|