@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 +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/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
|
|
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
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
334
|
+
activeSocket = void 0;
|
|
335
|
+
finish(true);
|
|
294
336
|
});
|
|
295
337
|
socket.once("error", () => {
|
|
296
|
-
|
|
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,
|
|
352
|
+
function retryPortProbe(socket, deadline, attempt, finish, setRetryTimer) {
|
|
303
353
|
socket.destroy();
|
|
304
354
|
if (Date.now() >= deadline) {
|
|
305
|
-
|
|
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",
|
|
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
|
-
|
|
976
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
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
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1425
|
+
});
|
|
1426
|
+
await runUntilStopped(session, options, eventLimit);
|
|
1427
|
+
if (options.format === "json") {
|
|
1428
|
+
writeJson({ events });
|
|
1325
1429
|
}
|
|
1326
|
-
}
|
|
1327
|
-
|
|
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(
|
|
1452
|
+
stopReason = await waitForStop([abort, eventLimit, duration]);
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
failed = true;
|
|
1455
|
+
throw error;
|
|
1350
1456
|
} finally {
|
|
1351
1457
|
abort.cleanup();
|
|
1352
|
-
|
|
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(
|
|
1356
|
-
|
|
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
|
|
1363
|
-
|
|
1364
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|