@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/README.md
CHANGED
|
@@ -85,7 +85,7 @@ Open a tunnel for one app and keep running until interrupted.
|
|
|
85
85
|
```bash
|
|
86
86
|
cf-debugger start --region eu10 --org my-org --space dev --app my-app
|
|
87
87
|
cf-debugger start --region eu10 --org my-org --space dev --app my-app --port 9230
|
|
88
|
-
cf-debugger start --region eu10 --org my-org --space dev --app my-app --timeout
|
|
88
|
+
cf-debugger start --region eu10 --org my-org --space dev --app my-app --timeout 180 --verbose
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
| Flag | Description |
|
|
@@ -95,9 +95,13 @@ cf-debugger start --region eu10 --org my-org --space dev --app my-app --timeout
|
|
|
95
95
|
| `--space <name>` | **Required.** CF space name |
|
|
96
96
|
| `--app <name>` | **Required.** CF app name |
|
|
97
97
|
| `--port <number>` | Preferred local port (auto-assigned in `20000–20999` if omitted) |
|
|
98
|
-
| `--timeout <seconds>` | Tunnel-ready timeout (default: `
|
|
98
|
+
| `--timeout <seconds>` | Tunnel-ready timeout (default: `180`) |
|
|
99
99
|
| `--verbose` | Print every status transition |
|
|
100
100
|
|
|
101
|
+
Cloud Foundry startup commands (`api`, `auth`, `target`, SSH checks, app
|
|
102
|
+
restart, and the one-shot SIGUSR1 SSH command) each allow up to 180 seconds.
|
|
103
|
+
`--timeout` controls the subsequent local tunnel-readiness probe separately.
|
|
104
|
+
|
|
101
105
|
### ⏹️ `cf-debugger stop`
|
|
102
106
|
|
|
103
107
|
Stop a specific session or everything at once.
|
|
@@ -188,7 +192,7 @@ await handle.dispose();
|
|
|
188
192
|
| `CF_LOGIN_FAILED` | `cf api` / `cf auth` rejected the credentials |
|
|
189
193
|
| `CF_TARGET_FAILED` | Org or space not reachable |
|
|
190
194
|
| `SSH_NOT_ENABLED` | SSH disabled at space or app level and could not be enabled |
|
|
191
|
-
| `USR1_SIGNAL_FAILED` | Remote `kill -s USR1`
|
|
195
|
+
| `USR1_SIGNAL_FAILED` | Remote `kill -s USR1` failed, timed out, or was terminated by a signal |
|
|
192
196
|
| `TUNNEL_NOT_READY` | Inspector didn't respond on port 9229 before timeout |
|
|
193
197
|
| `PORT_UNAVAILABLE` | Preferred local port is taken and could not be freed |
|
|
194
198
|
|
package/dist/cli.js
CHANGED
|
@@ -4,9 +4,60 @@
|
|
|
4
4
|
import process6 from "process";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
|
-
// src/
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
7
|
+
// src/cloud-foundry/commands.ts
|
|
8
|
+
import { execFile as execFile2 } from "child_process";
|
|
9
|
+
import { promisify as promisify2 } from "util";
|
|
10
|
+
|
|
11
|
+
// src/regions.ts
|
|
12
|
+
var REGION_API_ENDPOINTS = {
|
|
13
|
+
ae01: "https://api.cf.ae01.hana.ondemand.com",
|
|
14
|
+
ap01: "https://api.cf.ap01.hana.ondemand.com",
|
|
15
|
+
ap10: "https://api.cf.ap10.hana.ondemand.com",
|
|
16
|
+
ap11: "https://api.cf.ap11.hana.ondemand.com",
|
|
17
|
+
ap12: "https://api.cf.ap12.hana.ondemand.com",
|
|
18
|
+
ap20: "https://api.cf.ap20.hana.ondemand.com",
|
|
19
|
+
ap21: "https://api.cf.ap21.hana.ondemand.com",
|
|
20
|
+
ap30: "https://api.cf.ap30.hana.ondemand.com",
|
|
21
|
+
br10: "https://api.cf.br10.hana.ondemand.com",
|
|
22
|
+
br20: "https://api.cf.br20.hana.ondemand.com",
|
|
23
|
+
br30: "https://api.cf.br30.hana.ondemand.com",
|
|
24
|
+
ca10: "https://api.cf.ca10.hana.ondemand.com",
|
|
25
|
+
ca20: "https://api.cf.ca20.hana.ondemand.com",
|
|
26
|
+
ch20: "https://api.cf.ch20.hana.ondemand.com",
|
|
27
|
+
eu10: "https://api.cf.eu10.hana.ondemand.com",
|
|
28
|
+
eu11: "https://api.cf.eu11.hana.ondemand.com",
|
|
29
|
+
eu12: "https://api.cf.eu12.hana.ondemand.com",
|
|
30
|
+
eu20: "https://api.cf.eu20.hana.ondemand.com",
|
|
31
|
+
eu21: "https://api.cf.eu21.hana.ondemand.com",
|
|
32
|
+
eu30: "https://api.cf.eu30.hana.ondemand.com",
|
|
33
|
+
eu31: "https://api.cf.eu31.hana.ondemand.com",
|
|
34
|
+
in30: "https://api.cf.in30.hana.ondemand.com",
|
|
35
|
+
jp10: "https://api.cf.jp10.hana.ondemand.com",
|
|
36
|
+
jp20: "https://api.cf.jp20.hana.ondemand.com",
|
|
37
|
+
jp30: "https://api.cf.jp30.hana.ondemand.com",
|
|
38
|
+
kr30: "https://api.cf.kr30.hana.ondemand.com",
|
|
39
|
+
us10: "https://api.cf.us10.hana.ondemand.com",
|
|
40
|
+
us11: "https://api.cf.us11.hana.ondemand.com",
|
|
41
|
+
us20: "https://api.cf.us20.hana.ondemand.com",
|
|
42
|
+
us21: "https://api.cf.us21.hana.ondemand.com",
|
|
43
|
+
us30: "https://api.cf.us30.hana.ondemand.com",
|
|
44
|
+
us31: "https://api.cf.us31.hana.ondemand.com"
|
|
45
|
+
};
|
|
46
|
+
function resolveApiEndpoint(regionKey, override) {
|
|
47
|
+
if (override !== void 0 && override !== "") {
|
|
48
|
+
return override;
|
|
49
|
+
}
|
|
50
|
+
const endpoint = REGION_API_ENDPOINTS[regionKey];
|
|
51
|
+
if (endpoint === void 0) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Unknown region key: ${regionKey}. Pass \`apiEndpoint\` explicitly to override.`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return endpoint;
|
|
57
|
+
}
|
|
58
|
+
function listKnownRegionKeys() {
|
|
59
|
+
return Object.keys(REGION_API_ENDPOINTS);
|
|
60
|
+
}
|
|
10
61
|
|
|
11
62
|
// src/types.ts
|
|
12
63
|
var CfDebuggerError = class extends Error {
|
|
@@ -27,7 +78,7 @@ import { execFile } from "child_process";
|
|
|
27
78
|
import { promisify } from "util";
|
|
28
79
|
var execFileAsync = promisify(execFile);
|
|
29
80
|
var MAX_BUFFER = 16 * 1024 * 1024;
|
|
30
|
-
var
|
|
81
|
+
var DEFAULT_CF_COMMAND_TIMEOUT_MS = 18e4;
|
|
31
82
|
var REDACTED_ARG = "<redacted>";
|
|
32
83
|
function buildEnv(cfHome) {
|
|
33
84
|
return { ...process.env, CF_HOME: cfHome };
|
|
@@ -50,7 +101,7 @@ function formatArgsForError(args) {
|
|
|
50
101
|
}
|
|
51
102
|
return args.map((arg, index) => index === 0 ? arg : REDACTED_ARG).join(" ");
|
|
52
103
|
}
|
|
53
|
-
async function runCf(args, context, timeoutMs =
|
|
104
|
+
async function runCf(args, context, timeoutMs = DEFAULT_CF_COMMAND_TIMEOUT_MS) {
|
|
54
105
|
try {
|
|
55
106
|
const { stdout } = await execFileAsync(resolveBin(context), [...args], {
|
|
56
107
|
env: buildEnv(context.cfHome),
|
|
@@ -72,8 +123,9 @@ async function runCf(args, context, timeoutMs = CF_CLI_TIMEOUT_MS) {
|
|
|
72
123
|
}
|
|
73
124
|
|
|
74
125
|
// src/cloud-foundry/commands.ts
|
|
75
|
-
var
|
|
126
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
76
127
|
var CF_AUTH_MAX_ATTEMPTS = 3;
|
|
128
|
+
var CURRENT_TARGET_TIMEOUT_MS = 3e4;
|
|
77
129
|
async function cfApi(apiEndpoint, context) {
|
|
78
130
|
await runCf(["api", apiEndpoint], context);
|
|
79
131
|
}
|
|
@@ -137,13 +189,74 @@ async function cfEnableSsh(appName, context) {
|
|
|
137
189
|
}
|
|
138
190
|
}
|
|
139
191
|
async function cfRestartApp(appName, context) {
|
|
140
|
-
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;
|
|
141
255
|
}
|
|
142
256
|
|
|
143
257
|
// src/cloud-foundry/ssh.ts
|
|
144
258
|
import { spawn } from "child_process";
|
|
145
|
-
|
|
146
|
-
async function cfSshOneShot(appName, command, context) {
|
|
259
|
+
async function cfSshOneShot(appName, command, context, timeoutMs = DEFAULT_CF_COMMAND_TIMEOUT_MS) {
|
|
147
260
|
return await new Promise((resolve) => {
|
|
148
261
|
const child = spawn(resolveBin(context), ["ssh", appName, "-c", command], {
|
|
149
262
|
env: buildEnv(context.cfHome),
|
|
@@ -160,18 +273,20 @@ async function cfSshOneShot(appName, command, context) {
|
|
|
160
273
|
child.kill();
|
|
161
274
|
} catch {
|
|
162
275
|
}
|
|
163
|
-
resolve({ exitCode: null, stderr: stderrBuf });
|
|
164
|
-
},
|
|
276
|
+
resolve({ exitCode: null, stderr: stderrBuf, timedOutAfterMs: timeoutMs });
|
|
277
|
+
}, timeoutMs);
|
|
165
278
|
child.stderr.on("data", (data) => {
|
|
166
279
|
stderrBuf += data.toString();
|
|
167
280
|
});
|
|
168
|
-
child.on("close", (code) => {
|
|
281
|
+
child.on("close", (code, signal) => {
|
|
169
282
|
if (settled) {
|
|
170
283
|
return;
|
|
171
284
|
}
|
|
172
285
|
settled = true;
|
|
173
286
|
clearTimeout(timeout);
|
|
174
|
-
resolve(
|
|
287
|
+
resolve(
|
|
288
|
+
signal === null ? { exitCode: code, stderr: stderrBuf } : { exitCode: code, stderr: stderrBuf, signal }
|
|
289
|
+
);
|
|
175
290
|
});
|
|
176
291
|
child.on("error", (err) => {
|
|
177
292
|
if (settled) {
|
|
@@ -197,6 +312,10 @@ function spawnSshTunnel(appName, localPort, remotePort, context) {
|
|
|
197
312
|
});
|
|
198
313
|
}
|
|
199
314
|
|
|
315
|
+
// src/debug-session/start.ts
|
|
316
|
+
import { mkdir as mkdir3, rm } from "fs/promises";
|
|
317
|
+
import process4 from "process";
|
|
318
|
+
|
|
200
319
|
// src/paths.ts
|
|
201
320
|
import { homedir } from "os";
|
|
202
321
|
import { join } from "path";
|
|
@@ -218,13 +337,13 @@ function sessionCfHomeDir(sessionId) {
|
|
|
218
337
|
}
|
|
219
338
|
|
|
220
339
|
// src/network/ports.ts
|
|
221
|
-
import { execFile as
|
|
340
|
+
import { execFile as execFile3 } from "child_process";
|
|
222
341
|
import { createConnection, createServer } from "net";
|
|
223
|
-
import { promisify as
|
|
224
|
-
var
|
|
342
|
+
import { promisify as promisify3 } from "util";
|
|
343
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
225
344
|
async function findListeningPidsWithNetstat(port) {
|
|
226
345
|
try {
|
|
227
|
-
const { stdout } = await
|
|
346
|
+
const { stdout } = await execFileAsync3("netstat", ["-ano"]);
|
|
228
347
|
const pids = /* @__PURE__ */ new Set();
|
|
229
348
|
for (const line of stdout.split("\n")) {
|
|
230
349
|
if (!line.includes(`:${port.toString()}`) || !line.includes("LISTENING")) {
|
|
@@ -247,7 +366,7 @@ async function findListeningPidsWithNetstat(port) {
|
|
|
247
366
|
}
|
|
248
367
|
async function findListeningPidsWithLsof(port) {
|
|
249
368
|
try {
|
|
250
|
-
const { stdout } = await
|
|
369
|
+
const { stdout } = await execFileAsync3("lsof", ["-nP", "-t", "-i", `tcp:${port.toString()}`, "-sTCP:LISTEN"]);
|
|
251
370
|
return stdout.trim().split("\n").filter((line) => line.length > 0).map((line) => Number.parseInt(line, 10)).filter((pid) => !Number.isNaN(pid));
|
|
252
371
|
} catch {
|
|
253
372
|
return [];
|
|
@@ -313,7 +432,7 @@ async function killProcessOnPort(port) {
|
|
|
313
432
|
const pids = await findListeningPidsWithNetstat(port);
|
|
314
433
|
for (const pid of pids) {
|
|
315
434
|
try {
|
|
316
|
-
await
|
|
435
|
+
await execFileAsync3("taskkill", ["/F", "/PID", pid.toString()]);
|
|
317
436
|
} catch {
|
|
318
437
|
}
|
|
319
438
|
}
|
|
@@ -322,7 +441,7 @@ async function killProcessOnPort(port) {
|
|
|
322
441
|
return;
|
|
323
442
|
}
|
|
324
443
|
try {
|
|
325
|
-
const { stdout } = await
|
|
444
|
+
const { stdout } = await execFileAsync3("lsof", ["-t", "-i", `tcp:${portStr}`]);
|
|
326
445
|
const lines = stdout.trim().split("\n").filter((line) => line.length > 0);
|
|
327
446
|
for (const line of lines) {
|
|
328
447
|
const pid = Number.parseInt(line, 10);
|
|
@@ -338,54 +457,6 @@ async function killProcessOnPort(port) {
|
|
|
338
457
|
}
|
|
339
458
|
}
|
|
340
459
|
|
|
341
|
-
// src/regions.ts
|
|
342
|
-
var REGION_API_ENDPOINTS = {
|
|
343
|
-
ae01: "https://api.cf.ae01.hana.ondemand.com",
|
|
344
|
-
ap01: "https://api.cf.ap01.hana.ondemand.com",
|
|
345
|
-
ap10: "https://api.cf.ap10.hana.ondemand.com",
|
|
346
|
-
ap11: "https://api.cf.ap11.hana.ondemand.com",
|
|
347
|
-
ap12: "https://api.cf.ap12.hana.ondemand.com",
|
|
348
|
-
ap20: "https://api.cf.ap20.hana.ondemand.com",
|
|
349
|
-
ap21: "https://api.cf.ap21.hana.ondemand.com",
|
|
350
|
-
ap30: "https://api.cf.ap30.hana.ondemand.com",
|
|
351
|
-
br10: "https://api.cf.br10.hana.ondemand.com",
|
|
352
|
-
br20: "https://api.cf.br20.hana.ondemand.com",
|
|
353
|
-
br30: "https://api.cf.br30.hana.ondemand.com",
|
|
354
|
-
ca10: "https://api.cf.ca10.hana.ondemand.com",
|
|
355
|
-
ca20: "https://api.cf.ca20.hana.ondemand.com",
|
|
356
|
-
ch20: "https://api.cf.ch20.hana.ondemand.com",
|
|
357
|
-
eu10: "https://api.cf.eu10.hana.ondemand.com",
|
|
358
|
-
eu11: "https://api.cf.eu11.hana.ondemand.com",
|
|
359
|
-
eu12: "https://api.cf.eu12.hana.ondemand.com",
|
|
360
|
-
eu20: "https://api.cf.eu20.hana.ondemand.com",
|
|
361
|
-
eu21: "https://api.cf.eu21.hana.ondemand.com",
|
|
362
|
-
eu30: "https://api.cf.eu30.hana.ondemand.com",
|
|
363
|
-
eu31: "https://api.cf.eu31.hana.ondemand.com",
|
|
364
|
-
in30: "https://api.cf.in30.hana.ondemand.com",
|
|
365
|
-
jp10: "https://api.cf.jp10.hana.ondemand.com",
|
|
366
|
-
jp20: "https://api.cf.jp20.hana.ondemand.com",
|
|
367
|
-
jp30: "https://api.cf.jp30.hana.ondemand.com",
|
|
368
|
-
kr30: "https://api.cf.kr30.hana.ondemand.com",
|
|
369
|
-
us10: "https://api.cf.us10.hana.ondemand.com",
|
|
370
|
-
us11: "https://api.cf.us11.hana.ondemand.com",
|
|
371
|
-
us20: "https://api.cf.us20.hana.ondemand.com",
|
|
372
|
-
us21: "https://api.cf.us21.hana.ondemand.com",
|
|
373
|
-
us30: "https://api.cf.us30.hana.ondemand.com",
|
|
374
|
-
us31: "https://api.cf.us31.hana.ondemand.com"
|
|
375
|
-
};
|
|
376
|
-
function resolveApiEndpoint(regionKey, override) {
|
|
377
|
-
if (override !== void 0 && override !== "") {
|
|
378
|
-
return override;
|
|
379
|
-
}
|
|
380
|
-
const endpoint = REGION_API_ENDPOINTS[regionKey];
|
|
381
|
-
if (endpoint === void 0) {
|
|
382
|
-
throw new Error(
|
|
383
|
-
`Unknown region key: ${regionKey}. Pass \`apiEndpoint\` explicitly to override.`
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
return endpoint;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
460
|
// src/session-state/store.ts
|
|
390
461
|
import { randomUUID } from "crypto";
|
|
391
462
|
import { mkdir as mkdir2, readFile, rename, writeFile } from "fs/promises";
|
|
@@ -680,7 +751,7 @@ async function removeSession(sessionId) {
|
|
|
680
751
|
}
|
|
681
752
|
|
|
682
753
|
// src/debug-session/constants.ts
|
|
683
|
-
var DEFAULT_TUNNEL_READY_TIMEOUT_MS =
|
|
754
|
+
var DEFAULT_TUNNEL_READY_TIMEOUT_MS = 18e4;
|
|
684
755
|
var POST_USR1_DELAY_MS = 300;
|
|
685
756
|
var PORT_CLEANUP_DELAY_MS = 600;
|
|
686
757
|
var CHILD_SIGTERM_GRACE_MS = 2e3;
|
|
@@ -743,6 +814,19 @@ async function killProcessGroupOrProc(child, timeoutMs = CHILD_SIGTERM_GRACE_MS)
|
|
|
743
814
|
}
|
|
744
815
|
|
|
745
816
|
// src/debug-session/start.ts
|
|
817
|
+
function signalFailureDetail(result) {
|
|
818
|
+
if (result.timedOutAfterMs !== void 0) {
|
|
819
|
+
return `timed out after ${(result.timedOutAfterMs / 1e3).toString()}s`;
|
|
820
|
+
}
|
|
821
|
+
const stderr = result.stderr.trim();
|
|
822
|
+
if (stderr.length > 0) {
|
|
823
|
+
return stderr;
|
|
824
|
+
}
|
|
825
|
+
if (result.signal !== void 0) {
|
|
826
|
+
return `terminated by signal ${result.signal}`;
|
|
827
|
+
}
|
|
828
|
+
return `exit code ${String(result.exitCode)}`;
|
|
829
|
+
}
|
|
746
830
|
function checkAbort(signal) {
|
|
747
831
|
if (signal?.aborted) {
|
|
748
832
|
throw new CfDebuggerError("ABORTED", "Operation aborted by caller");
|
|
@@ -802,10 +886,9 @@ async function signalRemoteNode(options, context, sessionId, emit) {
|
|
|
802
886
|
if (signalResult.exitCode === 0) {
|
|
803
887
|
return;
|
|
804
888
|
}
|
|
805
|
-
const detail = signalResult.stderr.trim().length > 0 ? signalResult.stderr.trim() : `exit code ${String(signalResult.exitCode)}`;
|
|
806
889
|
throw new CfDebuggerError(
|
|
807
890
|
"USR1_SIGNAL_FAILED",
|
|
808
|
-
`Failed to send SIGUSR1 to the Node.js process on ${options.app}: ${
|
|
891
|
+
`Failed to send SIGUSR1 to the Node.js process on ${options.app}: ${signalFailureDetail(signalResult)}`,
|
|
809
892
|
signalResult.stderr
|
|
810
893
|
);
|
|
811
894
|
}
|
|
@@ -832,10 +915,9 @@ async function retryRemoteSignal(options, context, sessionId, emit) {
|
|
|
832
915
|
if (retrySignalResult.exitCode === 0) {
|
|
833
916
|
return;
|
|
834
917
|
}
|
|
835
|
-
const detail = retrySignalResult.stderr.trim().length > 0 ? retrySignalResult.stderr.trim() : `exit code ${String(retrySignalResult.exitCode)}`;
|
|
836
918
|
throw new CfDebuggerError(
|
|
837
919
|
"USR1_SIGNAL_FAILED",
|
|
838
|
-
`Failed to send SIGUSR1 to the Node.js process on ${options.app} after enabling SSH: ${
|
|
920
|
+
`Failed to send SIGUSR1 to the Node.js process on ${options.app} after enabling SSH: ${signalFailureDetail(retrySignalResult)}`,
|
|
839
921
|
retrySignalResult.stderr
|
|
840
922
|
);
|
|
841
923
|
}
|
|
@@ -1074,10 +1156,8 @@ function logStatus(verbose, status, message) {
|
|
|
1074
1156
|
}
|
|
1075
1157
|
}
|
|
1076
1158
|
async function handleStart(opts) {
|
|
1077
|
-
const region = readRequiredOption(opts.region, "--region");
|
|
1078
|
-
const org = readRequiredOption(opts.org, "--org");
|
|
1079
|
-
const space = readRequiredOption(opts.space, "--space");
|
|
1080
1159
|
const app = readRequiredOption(opts.app, "--app");
|
|
1160
|
+
const key = await resolveSessionKey({ ...opts, app });
|
|
1081
1161
|
const verbose = opts.verbose ?? false;
|
|
1082
1162
|
const preferredPort = parseOptionalPort(opts.port);
|
|
1083
1163
|
const tunnelReadyTimeoutMs = parseOptionalTimeout(opts.timeout);
|
|
@@ -1098,9 +1178,9 @@ Aborting startup for ${app}...
|
|
|
1098
1178
|
let handle;
|
|
1099
1179
|
try {
|
|
1100
1180
|
handle = await startDebugger({
|
|
1101
|
-
region,
|
|
1102
|
-
org,
|
|
1103
|
-
space,
|
|
1181
|
+
region: key.region,
|
|
1182
|
+
org: key.org,
|
|
1183
|
+
space: key.space,
|
|
1104
1184
|
app,
|
|
1105
1185
|
verbose,
|
|
1106
1186
|
signal: abortController.signal,
|
|
@@ -1115,7 +1195,7 @@ Aborting startup for ${app}...
|
|
|
1115
1195
|
process6.off("SIGTERM", startupSigterm);
|
|
1116
1196
|
}
|
|
1117
1197
|
process6.stdout.write(
|
|
1118
|
-
`Debugger ready for ${app} (${region}/${org}/${space}).
|
|
1198
|
+
`Debugger ready for ${app} (${key.region}/${key.org}/${key.space}).
|
|
1119
1199
|
Local port: ${handle.session.localPort.toString()}
|
|
1120
1200
|
Remote port: ${handle.session.remotePort.toString()}
|
|
1121
1201
|
Session id: ${handle.session.sessionId}
|
|
@@ -1153,16 +1233,58 @@ Stopping debugger for ${app}...
|
|
|
1153
1233
|
await dispose();
|
|
1154
1234
|
process6.exit(code ?? 0);
|
|
1155
1235
|
}
|
|
1156
|
-
function
|
|
1157
|
-
|
|
1236
|
+
function hasText(value) {
|
|
1237
|
+
return optionalText(value) !== void 0;
|
|
1238
|
+
}
|
|
1239
|
+
function optionalText(value) {
|
|
1240
|
+
const trimmed = value?.trim();
|
|
1241
|
+
return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
|
|
1242
|
+
}
|
|
1243
|
+
function currentCfOptions() {
|
|
1244
|
+
const command = process6.env["CF_DEBUGGER_CF_BIN"];
|
|
1245
|
+
return command === void 0 ? void 0 : { command };
|
|
1246
|
+
}
|
|
1247
|
+
async function resolveSessionKey(opts) {
|
|
1248
|
+
const app = readRequiredOption(opts.app, "--app");
|
|
1249
|
+
const region = optionalText(opts.region);
|
|
1250
|
+
const org = optionalText(opts.org);
|
|
1251
|
+
const space = optionalText(opts.space);
|
|
1252
|
+
if (region !== void 0 && org !== void 0 && space !== void 0) {
|
|
1158
1253
|
return {
|
|
1159
|
-
region
|
|
1160
|
-
org
|
|
1161
|
-
space
|
|
1162
|
-
app
|
|
1254
|
+
region,
|
|
1255
|
+
org,
|
|
1256
|
+
space,
|
|
1257
|
+
app
|
|
1163
1258
|
};
|
|
1164
1259
|
}
|
|
1165
|
-
|
|
1260
|
+
const current = await readCurrentCfTarget(currentCfOptions()).catch((error) => {
|
|
1261
|
+
throw new CfDebuggerError(
|
|
1262
|
+
"CF_TARGET_FAILED",
|
|
1263
|
+
"No current CF target found. Run `cf target -o <org> -s <space>` or pass --region/--org/--space.",
|
|
1264
|
+
error instanceof Error ? error.message : String(error)
|
|
1265
|
+
);
|
|
1266
|
+
});
|
|
1267
|
+
if (current === void 0) {
|
|
1268
|
+
throw new CfDebuggerError(
|
|
1269
|
+
"CF_TARGET_FAILED",
|
|
1270
|
+
"No current CF target found. Run `cf target -o <org> -s <space>` or pass --region/--org/--space."
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
return {
|
|
1274
|
+
region: region ?? requireCurrentCfRegion(current),
|
|
1275
|
+
org: org ?? current.org,
|
|
1276
|
+
space: space ?? current.space,
|
|
1277
|
+
app
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
async function resolveOptionalSessionKey(opts) {
|
|
1281
|
+
if (!hasText(opts.app)) {
|
|
1282
|
+
if (hasText(opts.region) || hasText(opts.org) || hasText(opts.space)) {
|
|
1283
|
+
readRequiredOption(opts.app, "--app");
|
|
1284
|
+
}
|
|
1285
|
+
return void 0;
|
|
1286
|
+
}
|
|
1287
|
+
return await resolveSessionKey(opts);
|
|
1166
1288
|
}
|
|
1167
1289
|
async function handleStop(opts) {
|
|
1168
1290
|
if (opts.all === true) {
|
|
@@ -1171,7 +1293,7 @@ async function handleStop(opts) {
|
|
|
1171
1293
|
`);
|
|
1172
1294
|
return;
|
|
1173
1295
|
}
|
|
1174
|
-
const key =
|
|
1296
|
+
const key = await resolveOptionalSessionKey(opts);
|
|
1175
1297
|
const result = await stopDebugger({
|
|
1176
1298
|
...opts.sessionId === void 0 ? {} : { sessionId: opts.sessionId },
|
|
1177
1299
|
...key === void 0 ? {} : { key }
|
|
@@ -1191,19 +1313,14 @@ async function handleList() {
|
|
|
1191
1313
|
`);
|
|
1192
1314
|
}
|
|
1193
1315
|
async function handleStatus(opts) {
|
|
1194
|
-
const session = await getSession(
|
|
1195
|
-
region: opts.region,
|
|
1196
|
-
org: opts.org,
|
|
1197
|
-
space: opts.space,
|
|
1198
|
-
app: opts.app
|
|
1199
|
-
});
|
|
1316
|
+
const session = await getSession(await resolveSessionKey(opts));
|
|
1200
1317
|
process6.stdout.write(`${JSON.stringify(session ?? null, null, 2)}
|
|
1201
1318
|
`);
|
|
1202
1319
|
}
|
|
1203
1320
|
async function main(argv) {
|
|
1204
1321
|
const program = new Command();
|
|
1205
1322
|
program.name("cf-debugger").description("Open an SSH debug tunnel to a SAP BTP Cloud Foundry app's Node.js inspector");
|
|
1206
|
-
program.command("start").description("Open a debug tunnel for one app").
|
|
1323
|
+
program.command("start").description("Open a debug tunnel for one app").option("--region <key>", "CF region key (default: current cf target)").option("--org <name>", "CF org name (default: current cf target)").option("--space <name>", "CF space name (default: current cf target)").requiredOption("--app <name>", "CF app name").option("--port <number>", "Preferred local port (auto-assigned if omitted)").option("--timeout <seconds>", "Tunnel-ready timeout in seconds (default: 180)").option("--verbose", "Print status transitions", false).action(async (opts) => {
|
|
1207
1324
|
await handleStart(opts);
|
|
1208
1325
|
});
|
|
1209
1326
|
program.command("stop").description("Stop one session (by key or id) or all sessions with --all").option("--region <key>").option("--org <name>").option("--space <name>").option("--app <name>").option("--session-id <id>").option("--all", "Stop every active session", false).action(async (opts) => {
|
|
@@ -1212,7 +1329,7 @@ async function main(argv) {
|
|
|
1212
1329
|
program.command("list").description("Print every active debugger session as JSON").action(async () => {
|
|
1213
1330
|
await handleList();
|
|
1214
1331
|
});
|
|
1215
|
-
program.command("status").description("Print one session by key as JSON (null if not active)").
|
|
1332
|
+
program.command("status").description("Print one session by key as JSON (null if not active)").option("--region <key>").option("--org <name>").option("--space <name>").requiredOption("--app <name>").action(async (opts) => {
|
|
1216
1333
|
await handleStatus(opts);
|
|
1217
1334
|
});
|
|
1218
1335
|
await program.parseAsync([...argv]);
|