@saptools/cf-debugger 0.1.7 → 0.1.9
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/README.md +7 -3
- package/dist/cli.js +213 -96
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +155 -73
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -18,12 +18,67 @@ var CfDebuggerError = class extends Error {
|
|
|
18
18
|
import { mkdir as mkdir3, rm } from "fs/promises";
|
|
19
19
|
import process4 from "process";
|
|
20
20
|
|
|
21
|
+
// src/cloud-foundry/commands.ts
|
|
22
|
+
import { execFile as execFile2 } from "child_process";
|
|
23
|
+
import { promisify as promisify2 } from "util";
|
|
24
|
+
|
|
25
|
+
// src/regions.ts
|
|
26
|
+
var REGION_API_ENDPOINTS = {
|
|
27
|
+
ae01: "https://api.cf.ae01.hana.ondemand.com",
|
|
28
|
+
ap01: "https://api.cf.ap01.hana.ondemand.com",
|
|
29
|
+
ap10: "https://api.cf.ap10.hana.ondemand.com",
|
|
30
|
+
ap11: "https://api.cf.ap11.hana.ondemand.com",
|
|
31
|
+
ap12: "https://api.cf.ap12.hana.ondemand.com",
|
|
32
|
+
ap20: "https://api.cf.ap20.hana.ondemand.com",
|
|
33
|
+
ap21: "https://api.cf.ap21.hana.ondemand.com",
|
|
34
|
+
ap30: "https://api.cf.ap30.hana.ondemand.com",
|
|
35
|
+
br10: "https://api.cf.br10.hana.ondemand.com",
|
|
36
|
+
br20: "https://api.cf.br20.hana.ondemand.com",
|
|
37
|
+
br30: "https://api.cf.br30.hana.ondemand.com",
|
|
38
|
+
ca10: "https://api.cf.ca10.hana.ondemand.com",
|
|
39
|
+
ca20: "https://api.cf.ca20.hana.ondemand.com",
|
|
40
|
+
ch20: "https://api.cf.ch20.hana.ondemand.com",
|
|
41
|
+
eu10: "https://api.cf.eu10.hana.ondemand.com",
|
|
42
|
+
eu11: "https://api.cf.eu11.hana.ondemand.com",
|
|
43
|
+
eu12: "https://api.cf.eu12.hana.ondemand.com",
|
|
44
|
+
eu20: "https://api.cf.eu20.hana.ondemand.com",
|
|
45
|
+
eu21: "https://api.cf.eu21.hana.ondemand.com",
|
|
46
|
+
eu30: "https://api.cf.eu30.hana.ondemand.com",
|
|
47
|
+
eu31: "https://api.cf.eu31.hana.ondemand.com",
|
|
48
|
+
in30: "https://api.cf.in30.hana.ondemand.com",
|
|
49
|
+
jp10: "https://api.cf.jp10.hana.ondemand.com",
|
|
50
|
+
jp20: "https://api.cf.jp20.hana.ondemand.com",
|
|
51
|
+
jp30: "https://api.cf.jp30.hana.ondemand.com",
|
|
52
|
+
kr30: "https://api.cf.kr30.hana.ondemand.com",
|
|
53
|
+
us10: "https://api.cf.us10.hana.ondemand.com",
|
|
54
|
+
us11: "https://api.cf.us11.hana.ondemand.com",
|
|
55
|
+
us20: "https://api.cf.us20.hana.ondemand.com",
|
|
56
|
+
us21: "https://api.cf.us21.hana.ondemand.com",
|
|
57
|
+
us30: "https://api.cf.us30.hana.ondemand.com",
|
|
58
|
+
us31: "https://api.cf.us31.hana.ondemand.com"
|
|
59
|
+
};
|
|
60
|
+
function resolveApiEndpoint(regionKey, override) {
|
|
61
|
+
if (override !== void 0 && override !== "") {
|
|
62
|
+
return override;
|
|
63
|
+
}
|
|
64
|
+
const endpoint = REGION_API_ENDPOINTS[regionKey];
|
|
65
|
+
if (endpoint === void 0) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Unknown region key: ${regionKey}. Pass \`apiEndpoint\` explicitly to override.`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return endpoint;
|
|
71
|
+
}
|
|
72
|
+
function listKnownRegionKeys() {
|
|
73
|
+
return Object.keys(REGION_API_ENDPOINTS);
|
|
74
|
+
}
|
|
75
|
+
|
|
21
76
|
// src/cloud-foundry/execute.ts
|
|
22
77
|
import { execFile } from "child_process";
|
|
23
78
|
import { promisify } from "util";
|
|
24
79
|
var execFileAsync = promisify(execFile);
|
|
25
80
|
var MAX_BUFFER = 16 * 1024 * 1024;
|
|
26
|
-
var
|
|
81
|
+
var DEFAULT_CF_COMMAND_TIMEOUT_MS = 18e4;
|
|
27
82
|
var REDACTED_ARG = "<redacted>";
|
|
28
83
|
function buildEnv(cfHome) {
|
|
29
84
|
return { ...process.env, CF_HOME: cfHome };
|
|
@@ -46,7 +101,7 @@ function formatArgsForError(args) {
|
|
|
46
101
|
}
|
|
47
102
|
return args.map((arg, index) => index === 0 ? arg : REDACTED_ARG).join(" ");
|
|
48
103
|
}
|
|
49
|
-
async function runCf(args, context, timeoutMs =
|
|
104
|
+
async function runCf(args, context, timeoutMs = DEFAULT_CF_COMMAND_TIMEOUT_MS) {
|
|
50
105
|
try {
|
|
51
106
|
const { stdout } = await execFileAsync(resolveBin(context), [...args], {
|
|
52
107
|
env: buildEnv(context.cfHome),
|
|
@@ -68,8 +123,9 @@ async function runCf(args, context, timeoutMs = CF_CLI_TIMEOUT_MS) {
|
|
|
68
123
|
}
|
|
69
124
|
|
|
70
125
|
// src/cloud-foundry/commands.ts
|
|
71
|
-
var
|
|
126
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
72
127
|
var CF_AUTH_MAX_ATTEMPTS = 3;
|
|
128
|
+
var CURRENT_TARGET_TIMEOUT_MS = 3e4;
|
|
73
129
|
async function cfApi(apiEndpoint, context) {
|
|
74
130
|
await runCf(["api", apiEndpoint], context);
|
|
75
131
|
}
|
|
@@ -133,13 +189,74 @@ async function cfEnableSsh(appName, context) {
|
|
|
133
189
|
}
|
|
134
190
|
}
|
|
135
191
|
async function cfRestartApp(appName, context) {
|
|
136
|
-
await runCf(["restart", appName], context
|
|
192
|
+
await runCf(["restart", appName], context);
|
|
193
|
+
}
|
|
194
|
+
async function readCurrentCfTarget(options = {}) {
|
|
195
|
+
try {
|
|
196
|
+
const { stdout } = await execFileAsync2(options.command ?? process.env["CF_DEBUGGER_CF_BIN"] ?? "cf", ["target"], {
|
|
197
|
+
env: options.env ?? process.env,
|
|
198
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
199
|
+
timeout: options.timeoutMs ?? CURRENT_TARGET_TIMEOUT_MS
|
|
200
|
+
});
|
|
201
|
+
return parseCurrentCfTarget(stdout);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
204
|
+
throw new CfDebuggerError("CF_TARGET_FAILED", `cf target failed: ${message}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function parseCurrentCfTarget(stdout) {
|
|
208
|
+
const fields = parseTargetFields(stdout);
|
|
209
|
+
const apiEndpoint = fields.get("api endpoint");
|
|
210
|
+
const org = fields.get("org");
|
|
211
|
+
const space = fields.get("space");
|
|
212
|
+
if (!isPresent(apiEndpoint) || !isPresent(org) || !isPresent(space)) {
|
|
213
|
+
return void 0;
|
|
214
|
+
}
|
|
215
|
+
const region = regionKeyForApiEndpoint(apiEndpoint);
|
|
216
|
+
return {
|
|
217
|
+
apiEndpoint,
|
|
218
|
+
...region === void 0 ? {} : { region },
|
|
219
|
+
org,
|
|
220
|
+
space
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function requireCurrentCfRegion(target, instruction = "Pass --region explicitly.") {
|
|
224
|
+
if (target.region !== void 0) {
|
|
225
|
+
return target.region;
|
|
226
|
+
}
|
|
227
|
+
throw new CfDebuggerError(
|
|
228
|
+
"CF_TARGET_FAILED",
|
|
229
|
+
`Current CF API endpoint "${target.apiEndpoint}" does not match a known SAP region. ${instruction}`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
function parseTargetFields(stdout) {
|
|
233
|
+
return new Map(
|
|
234
|
+
stdout.split("\n").map((line) => {
|
|
235
|
+
const separator = line.indexOf(":");
|
|
236
|
+
if (separator < 0) {
|
|
237
|
+
return void 0;
|
|
238
|
+
}
|
|
239
|
+
return [
|
|
240
|
+
line.slice(0, separator).trim().toLowerCase(),
|
|
241
|
+
line.slice(separator + 1).trim()
|
|
242
|
+
];
|
|
243
|
+
}).filter((field) => field !== void 0)
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
function regionKeyForApiEndpoint(apiEndpoint) {
|
|
247
|
+
const normalized = normalizeApiEndpoint(apiEndpoint);
|
|
248
|
+
return listKnownRegionKeys().find((key) => normalizeApiEndpoint(resolveApiEndpoint(key)) === normalized);
|
|
249
|
+
}
|
|
250
|
+
function normalizeApiEndpoint(apiEndpoint) {
|
|
251
|
+
return apiEndpoint.trim().replace(/\/+$/, "").toLowerCase();
|
|
252
|
+
}
|
|
253
|
+
function isPresent(value) {
|
|
254
|
+
return value !== void 0 && value.length > 0;
|
|
137
255
|
}
|
|
138
256
|
|
|
139
257
|
// src/cloud-foundry/ssh.ts
|
|
140
258
|
import { spawn } from "child_process";
|
|
141
|
-
|
|
142
|
-
async function cfSshOneShot(appName, command, context) {
|
|
259
|
+
async function cfSshOneShot(appName, command, context, timeoutMs = DEFAULT_CF_COMMAND_TIMEOUT_MS) {
|
|
143
260
|
return await new Promise((resolve) => {
|
|
144
261
|
const child = spawn(resolveBin(context), ["ssh", appName, "-c", command], {
|
|
145
262
|
env: buildEnv(context.cfHome),
|
|
@@ -156,18 +273,20 @@ async function cfSshOneShot(appName, command, context) {
|
|
|
156
273
|
child.kill();
|
|
157
274
|
} catch {
|
|
158
275
|
}
|
|
159
|
-
resolve({ exitCode: null, stderr: stderrBuf });
|
|
160
|
-
},
|
|
276
|
+
resolve({ exitCode: null, stderr: stderrBuf, timedOutAfterMs: timeoutMs });
|
|
277
|
+
}, timeoutMs);
|
|
161
278
|
child.stderr.on("data", (data) => {
|
|
162
279
|
stderrBuf += data.toString();
|
|
163
280
|
});
|
|
164
|
-
child.on("close", (code) => {
|
|
281
|
+
child.on("close", (code, signal) => {
|
|
165
282
|
if (settled) {
|
|
166
283
|
return;
|
|
167
284
|
}
|
|
168
285
|
settled = true;
|
|
169
286
|
clearTimeout(timeout);
|
|
170
|
-
resolve(
|
|
287
|
+
resolve(
|
|
288
|
+
signal === null ? { exitCode: code, stderr: stderrBuf } : { exitCode: code, stderr: stderrBuf, signal }
|
|
289
|
+
);
|
|
171
290
|
});
|
|
172
291
|
child.on("error", (err) => {
|
|
173
292
|
if (settled) {
|
|
@@ -214,13 +333,13 @@ function sessionCfHomeDir(sessionId) {
|
|
|
214
333
|
}
|
|
215
334
|
|
|
216
335
|
// src/network/ports.ts
|
|
217
|
-
import { execFile as
|
|
336
|
+
import { execFile as execFile3 } from "child_process";
|
|
218
337
|
import { createConnection, createServer } from "net";
|
|
219
|
-
import { promisify as
|
|
220
|
-
var
|
|
338
|
+
import { promisify as promisify3 } from "util";
|
|
339
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
221
340
|
async function findListeningPidsWithNetstat(port) {
|
|
222
341
|
try {
|
|
223
|
-
const { stdout } = await
|
|
342
|
+
const { stdout } = await execFileAsync3("netstat", ["-ano"]);
|
|
224
343
|
const pids = /* @__PURE__ */ new Set();
|
|
225
344
|
for (const line of stdout.split("\n")) {
|
|
226
345
|
if (!line.includes(`:${port.toString()}`) || !line.includes("LISTENING")) {
|
|
@@ -243,7 +362,7 @@ async function findListeningPidsWithNetstat(port) {
|
|
|
243
362
|
}
|
|
244
363
|
async function findListeningPidsWithLsof(port) {
|
|
245
364
|
try {
|
|
246
|
-
const { stdout } = await
|
|
365
|
+
const { stdout } = await execFileAsync3("lsof", ["-nP", "-t", "-i", `tcp:${port.toString()}`, "-sTCP:LISTEN"]);
|
|
247
366
|
return stdout.trim().split("\n").filter((line) => line.length > 0).map((line) => Number.parseInt(line, 10)).filter((pid) => !Number.isNaN(pid));
|
|
248
367
|
} catch {
|
|
249
368
|
return [];
|
|
@@ -309,7 +428,7 @@ async function killProcessOnPort(port) {
|
|
|
309
428
|
const pids = await findListeningPidsWithNetstat(port);
|
|
310
429
|
for (const pid of pids) {
|
|
311
430
|
try {
|
|
312
|
-
await
|
|
431
|
+
await execFileAsync3("taskkill", ["/F", "/PID", pid.toString()]);
|
|
313
432
|
} catch {
|
|
314
433
|
}
|
|
315
434
|
}
|
|
@@ -318,7 +437,7 @@ async function killProcessOnPort(port) {
|
|
|
318
437
|
return;
|
|
319
438
|
}
|
|
320
439
|
try {
|
|
321
|
-
const { stdout } = await
|
|
440
|
+
const { stdout } = await execFileAsync3("lsof", ["-t", "-i", `tcp:${portStr}`]);
|
|
322
441
|
const lines = stdout.trim().split("\n").filter((line) => line.length > 0);
|
|
323
442
|
for (const line of lines) {
|
|
324
443
|
const pid = Number.parseInt(line, 10);
|
|
@@ -334,57 +453,6 @@ async function killProcessOnPort(port) {
|
|
|
334
453
|
}
|
|
335
454
|
}
|
|
336
455
|
|
|
337
|
-
// src/regions.ts
|
|
338
|
-
var REGION_API_ENDPOINTS = {
|
|
339
|
-
ae01: "https://api.cf.ae01.hana.ondemand.com",
|
|
340
|
-
ap01: "https://api.cf.ap01.hana.ondemand.com",
|
|
341
|
-
ap10: "https://api.cf.ap10.hana.ondemand.com",
|
|
342
|
-
ap11: "https://api.cf.ap11.hana.ondemand.com",
|
|
343
|
-
ap12: "https://api.cf.ap12.hana.ondemand.com",
|
|
344
|
-
ap20: "https://api.cf.ap20.hana.ondemand.com",
|
|
345
|
-
ap21: "https://api.cf.ap21.hana.ondemand.com",
|
|
346
|
-
ap30: "https://api.cf.ap30.hana.ondemand.com",
|
|
347
|
-
br10: "https://api.cf.br10.hana.ondemand.com",
|
|
348
|
-
br20: "https://api.cf.br20.hana.ondemand.com",
|
|
349
|
-
br30: "https://api.cf.br30.hana.ondemand.com",
|
|
350
|
-
ca10: "https://api.cf.ca10.hana.ondemand.com",
|
|
351
|
-
ca20: "https://api.cf.ca20.hana.ondemand.com",
|
|
352
|
-
ch20: "https://api.cf.ch20.hana.ondemand.com",
|
|
353
|
-
eu10: "https://api.cf.eu10.hana.ondemand.com",
|
|
354
|
-
eu11: "https://api.cf.eu11.hana.ondemand.com",
|
|
355
|
-
eu12: "https://api.cf.eu12.hana.ondemand.com",
|
|
356
|
-
eu20: "https://api.cf.eu20.hana.ondemand.com",
|
|
357
|
-
eu21: "https://api.cf.eu21.hana.ondemand.com",
|
|
358
|
-
eu30: "https://api.cf.eu30.hana.ondemand.com",
|
|
359
|
-
eu31: "https://api.cf.eu31.hana.ondemand.com",
|
|
360
|
-
in30: "https://api.cf.in30.hana.ondemand.com",
|
|
361
|
-
jp10: "https://api.cf.jp10.hana.ondemand.com",
|
|
362
|
-
jp20: "https://api.cf.jp20.hana.ondemand.com",
|
|
363
|
-
jp30: "https://api.cf.jp30.hana.ondemand.com",
|
|
364
|
-
kr30: "https://api.cf.kr30.hana.ondemand.com",
|
|
365
|
-
us10: "https://api.cf.us10.hana.ondemand.com",
|
|
366
|
-
us11: "https://api.cf.us11.hana.ondemand.com",
|
|
367
|
-
us20: "https://api.cf.us20.hana.ondemand.com",
|
|
368
|
-
us21: "https://api.cf.us21.hana.ondemand.com",
|
|
369
|
-
us30: "https://api.cf.us30.hana.ondemand.com",
|
|
370
|
-
us31: "https://api.cf.us31.hana.ondemand.com"
|
|
371
|
-
};
|
|
372
|
-
function resolveApiEndpoint(regionKey, override) {
|
|
373
|
-
if (override !== void 0 && override !== "") {
|
|
374
|
-
return override;
|
|
375
|
-
}
|
|
376
|
-
const endpoint = REGION_API_ENDPOINTS[regionKey];
|
|
377
|
-
if (endpoint === void 0) {
|
|
378
|
-
throw new Error(
|
|
379
|
-
`Unknown region key: ${regionKey}. Pass \`apiEndpoint\` explicitly to override.`
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
return endpoint;
|
|
383
|
-
}
|
|
384
|
-
function listKnownRegionKeys() {
|
|
385
|
-
return Object.keys(REGION_API_ENDPOINTS);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
456
|
// src/session-state/store.ts
|
|
389
457
|
import { randomUUID } from "crypto";
|
|
390
458
|
import { mkdir as mkdir2, readFile, rename, writeFile } from "fs/promises";
|
|
@@ -679,7 +747,7 @@ async function removeSession(sessionId) {
|
|
|
679
747
|
}
|
|
680
748
|
|
|
681
749
|
// src/debug-session/constants.ts
|
|
682
|
-
var DEFAULT_TUNNEL_READY_TIMEOUT_MS =
|
|
750
|
+
var DEFAULT_TUNNEL_READY_TIMEOUT_MS = 18e4;
|
|
683
751
|
var POST_USR1_DELAY_MS = 300;
|
|
684
752
|
var PORT_CLEANUP_DELAY_MS = 600;
|
|
685
753
|
var CHILD_SIGTERM_GRACE_MS = 2e3;
|
|
@@ -742,6 +810,19 @@ async function killProcessGroupOrProc(child, timeoutMs = CHILD_SIGTERM_GRACE_MS)
|
|
|
742
810
|
}
|
|
743
811
|
|
|
744
812
|
// src/debug-session/start.ts
|
|
813
|
+
function signalFailureDetail(result) {
|
|
814
|
+
if (result.timedOutAfterMs !== void 0) {
|
|
815
|
+
return `timed out after ${(result.timedOutAfterMs / 1e3).toString()}s`;
|
|
816
|
+
}
|
|
817
|
+
const stderr = result.stderr.trim();
|
|
818
|
+
if (stderr.length > 0) {
|
|
819
|
+
return stderr;
|
|
820
|
+
}
|
|
821
|
+
if (result.signal !== void 0) {
|
|
822
|
+
return `terminated by signal ${result.signal}`;
|
|
823
|
+
}
|
|
824
|
+
return `exit code ${String(result.exitCode)}`;
|
|
825
|
+
}
|
|
745
826
|
function checkAbort(signal) {
|
|
746
827
|
if (signal?.aborted) {
|
|
747
828
|
throw new CfDebuggerError("ABORTED", "Operation aborted by caller");
|
|
@@ -801,10 +882,9 @@ async function signalRemoteNode(options, context, sessionId, emit) {
|
|
|
801
882
|
if (signalResult.exitCode === 0) {
|
|
802
883
|
return;
|
|
803
884
|
}
|
|
804
|
-
const detail = signalResult.stderr.trim().length > 0 ? signalResult.stderr.trim() : `exit code ${String(signalResult.exitCode)}`;
|
|
805
885
|
throw new CfDebuggerError(
|
|
806
886
|
"USR1_SIGNAL_FAILED",
|
|
807
|
-
`Failed to send SIGUSR1 to the Node.js process on ${options.app}: ${
|
|
887
|
+
`Failed to send SIGUSR1 to the Node.js process on ${options.app}: ${signalFailureDetail(signalResult)}`,
|
|
808
888
|
signalResult.stderr
|
|
809
889
|
);
|
|
810
890
|
}
|
|
@@ -831,10 +911,9 @@ async function retryRemoteSignal(options, context, sessionId, emit) {
|
|
|
831
911
|
if (retrySignalResult.exitCode === 0) {
|
|
832
912
|
return;
|
|
833
913
|
}
|
|
834
|
-
const detail = retrySignalResult.stderr.trim().length > 0 ? retrySignalResult.stderr.trim() : `exit code ${String(retrySignalResult.exitCode)}`;
|
|
835
914
|
throw new CfDebuggerError(
|
|
836
915
|
"USR1_SIGNAL_FAILED",
|
|
837
|
-
`Failed to send SIGUSR1 to the Node.js process on ${options.app} after enabling SSH: ${
|
|
916
|
+
`Failed to send SIGUSR1 to the Node.js process on ${options.app} after enabling SSH: ${signalFailureDetail(retrySignalResult)}`,
|
|
838
917
|
retrySignalResult.stderr
|
|
839
918
|
);
|
|
840
919
|
}
|
|
@@ -1036,6 +1115,9 @@ export {
|
|
|
1036
1115
|
getSession,
|
|
1037
1116
|
listKnownRegionKeys,
|
|
1038
1117
|
listSessions,
|
|
1118
|
+
parseCurrentCfTarget,
|
|
1119
|
+
readCurrentCfTarget,
|
|
1120
|
+
requireCurrentCfRegion,
|
|
1039
1121
|
resolveApiEndpoint,
|
|
1040
1122
|
sessionKeyString,
|
|
1041
1123
|
startDebugger,
|