@saptools/cf-debugger 0.1.11 → 0.1.13
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 +1 -65
- package/dist/cli.js +100 -10
- package/dist/cli.js.map +1 -1
- package/dist/index.js +100 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Signal the remote process, enable SSH if needed, forward `9229` to a free local
|
|
|
12
12
|
[](https://packagephobia.com/result?p=@saptools/cf-debugger)
|
|
13
13
|
[](https://www.typescriptlang.org)
|
|
14
14
|
|
|
15
|
-
[Install](#-install) • [Quick Start](#-quick-start) • [CLI](#-cli) • [
|
|
15
|
+
[Install](#-install) • [Quick Start](#-quick-start) • [CLI](#-cli) • [How it works](#-how-it-works) • [FAQ](#-faq)
|
|
16
16
|
|
|
17
17
|
</div>
|
|
18
18
|
|
|
@@ -136,70 +136,6 @@ cf-debugger status --region eu10 --org my-org --space dev --app my-app
|
|
|
136
136
|
|
|
137
137
|
---
|
|
138
138
|
|
|
139
|
-
## 🧑💻 Programmatic Usage
|
|
140
|
-
|
|
141
|
-
```ts
|
|
142
|
-
import {
|
|
143
|
-
startDebugger,
|
|
144
|
-
stopDebugger,
|
|
145
|
-
listSessions,
|
|
146
|
-
getSession,
|
|
147
|
-
resolveApiEndpoint,
|
|
148
|
-
} from "@saptools/cf-debugger";
|
|
149
|
-
|
|
150
|
-
const handle = await startDebugger({
|
|
151
|
-
region: "eu10",
|
|
152
|
-
org: "my-org",
|
|
153
|
-
space: "dev",
|
|
154
|
-
app: "my-app",
|
|
155
|
-
email: process.env["SAP_EMAIL"],
|
|
156
|
-
password: process.env["SAP_PASSWORD"],
|
|
157
|
-
verbose: true,
|
|
158
|
-
onStatus: (status, message) => {
|
|
159
|
-
console.log(`[${status}]`, message ?? "");
|
|
160
|
-
},
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
console.log(`Attach your debugger to localhost:${handle.session.localPort}`);
|
|
164
|
-
|
|
165
|
-
// Later — shut the tunnel down and clean up state:
|
|
166
|
-
await handle.dispose();
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
<details>
|
|
170
|
-
<summary><b>📚 Full export list</b></summary>
|
|
171
|
-
|
|
172
|
-
| Export | Description |
|
|
173
|
-
| --- | --- |
|
|
174
|
-
| `startDebugger(options)` | Open a tunnel; returns a `DebuggerHandle` |
|
|
175
|
-
| `stopDebugger({ sessionId?, key? })` | Stop one session by id or by key |
|
|
176
|
-
| `stopAllDebuggers()` | Stop every session owned by this process/machine |
|
|
177
|
-
| `listSessions()` | Return every live session as `ActiveSession[]` |
|
|
178
|
-
| `getSession(key)` | Return one session matching `{ region, org, space, app }` |
|
|
179
|
-
| `resolveApiEndpoint(key, override?)` | Map a region key to its API endpoint |
|
|
180
|
-
| `sessionKeyString(key)` | Stable string form of a session key |
|
|
181
|
-
| `CfDebuggerError` | Rich error class with typed `code` |
|
|
182
|
-
|
|
183
|
-
</details>
|
|
184
|
-
|
|
185
|
-
<details>
|
|
186
|
-
<summary><b>🧪 Error codes</b></summary>
|
|
187
|
-
|
|
188
|
-
| Code | When |
|
|
189
|
-
| --- | --- |
|
|
190
|
-
| `MISSING_CREDENTIALS` | No `SAP_EMAIL` / `SAP_PASSWORD` in env or options |
|
|
191
|
-
| `SESSION_ALREADY_RUNNING` | A session already exists for the same `region/org/space/app` |
|
|
192
|
-
| `CF_LOGIN_FAILED` | `cf api` / `cf auth` rejected the credentials |
|
|
193
|
-
| `CF_TARGET_FAILED` | Org or space not reachable |
|
|
194
|
-
| `SSH_NOT_ENABLED` | SSH disabled at space or app level and could not be enabled |
|
|
195
|
-
| `USR1_SIGNAL_FAILED` | Remote `kill -s USR1` failed, timed out, or was terminated by a signal |
|
|
196
|
-
| `TUNNEL_NOT_READY` | Inspector didn't respond on port 9229 before timeout |
|
|
197
|
-
| `PORT_UNAVAILABLE` | Preferred local port is taken and could not be freed |
|
|
198
|
-
|
|
199
|
-
</details>
|
|
200
|
-
|
|
201
|
-
---
|
|
202
|
-
|
|
203
139
|
## 🔭 How it works
|
|
204
140
|
|
|
205
141
|
```
|
package/dist/cli.js
CHANGED
|
@@ -24,24 +24,41 @@ var REGION_API_ENDPOINTS = {
|
|
|
24
24
|
ca10: "https://api.cf.ca10.hana.ondemand.com",
|
|
25
25
|
ca20: "https://api.cf.ca20.hana.ondemand.com",
|
|
26
26
|
ch20: "https://api.cf.ch20.hana.ondemand.com",
|
|
27
|
+
cn20: "https://api.cf.cn20.platform.sapcloud.cn",
|
|
28
|
+
cn40: "https://api.cf.cn40.platform.sapcloud.cn",
|
|
29
|
+
eu01: "https://api.cf.eu01.hana.ondemand.com",
|
|
30
|
+
eu02: "https://api.cf.eu02.hana.ondemand.com",
|
|
27
31
|
eu10: "https://api.cf.eu10.hana.ondemand.com",
|
|
32
|
+
"eu10-002": "https://api.cf.eu10-002.hana.ondemand.com",
|
|
33
|
+
"eu10-003": "https://api.cf.eu10-003.hana.ondemand.com",
|
|
34
|
+
"eu10-004": "https://api.cf.eu10-004.hana.ondemand.com",
|
|
35
|
+
"eu10-005": "https://api.cf.eu10-005.hana.ondemand.com",
|
|
28
36
|
eu11: "https://api.cf.eu11.hana.ondemand.com",
|
|
29
|
-
|
|
37
|
+
eu13: "https://api.cf.eu13.hana.ondemand.com",
|
|
30
38
|
eu20: "https://api.cf.eu20.hana.ondemand.com",
|
|
31
|
-
|
|
39
|
+
"eu20-001": "https://api.cf.eu20-001.hana.ondemand.com",
|
|
40
|
+
"eu20-002": "https://api.cf.eu20-002.hana.ondemand.com",
|
|
41
|
+
eu22: "https://api.cf.eu22.hana.ondemand.com",
|
|
32
42
|
eu30: "https://api.cf.eu30.hana.ondemand.com",
|
|
33
|
-
|
|
43
|
+
il30: "https://api.cf.il30.hana.ondemand.com",
|
|
34
44
|
in30: "https://api.cf.in30.hana.ondemand.com",
|
|
45
|
+
jp01: "https://api.cf.jp01.hana.ondemand.com",
|
|
35
46
|
jp10: "https://api.cf.jp10.hana.ondemand.com",
|
|
36
47
|
jp20: "https://api.cf.jp20.hana.ondemand.com",
|
|
37
48
|
jp30: "https://api.cf.jp30.hana.ondemand.com",
|
|
38
|
-
|
|
49
|
+
jp31: "https://api.cf.jp31.hana.ondemand.com",
|
|
50
|
+
sa30: "https://api.cf.sa30.hana.ondemand.com",
|
|
51
|
+
sa31: "https://api.cf.sa31.hana.ondemand.com",
|
|
52
|
+
uk20: "https://api.cf.uk20.hana.ondemand.com",
|
|
53
|
+
us01: "https://api.cf.us01.hana.ondemand.com",
|
|
54
|
+
us02: "https://api.cf.us02.hana.ondemand.com",
|
|
39
55
|
us10: "https://api.cf.us10.hana.ondemand.com",
|
|
56
|
+
"us10-001": "https://api.cf.us10-001.hana.ondemand.com",
|
|
57
|
+
"us10-002": "https://api.cf.us10-002.hana.ondemand.com",
|
|
40
58
|
us11: "https://api.cf.us11.hana.ondemand.com",
|
|
41
59
|
us20: "https://api.cf.us20.hana.ondemand.com",
|
|
42
60
|
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"
|
|
61
|
+
us30: "https://api.cf.us30.hana.ondemand.com"
|
|
45
62
|
};
|
|
46
63
|
function resolveApiEndpoint(regionKey, override) {
|
|
47
64
|
if (override !== void 0 && override !== "") {
|
|
@@ -245,11 +262,19 @@ function parseTargetFields(stdout) {
|
|
|
245
262
|
}
|
|
246
263
|
function regionKeyForApiEndpoint(apiEndpoint) {
|
|
247
264
|
const normalized = normalizeApiEndpoint(apiEndpoint);
|
|
248
|
-
|
|
265
|
+
const known = listKnownRegionKeys().find((key) => normalizeApiEndpoint(resolveApiEndpoint(key)) === normalized);
|
|
266
|
+
if (known !== void 0) {
|
|
267
|
+
return known;
|
|
268
|
+
}
|
|
269
|
+
return regionKeyFromSapApiEndpoint(normalized);
|
|
249
270
|
}
|
|
250
271
|
function normalizeApiEndpoint(apiEndpoint) {
|
|
251
272
|
return apiEndpoint.trim().replace(/\/+$/, "").toLowerCase();
|
|
252
273
|
}
|
|
274
|
+
function regionKeyFromSapApiEndpoint(apiEndpoint) {
|
|
275
|
+
const match = /^https:\/\/api\.cf\.([a-z]{2}\d{2}(?:-\d{3})?)\.(?:hana\.ondemand\.com|platform\.sapcloud\.cn)$/.exec(apiEndpoint);
|
|
276
|
+
return match?.[1];
|
|
277
|
+
}
|
|
253
278
|
function isPresent(value) {
|
|
254
279
|
return value !== void 0 && value.length > 0;
|
|
255
280
|
}
|
|
@@ -338,6 +363,7 @@ function sessionCfHomeDir(sessionId) {
|
|
|
338
363
|
|
|
339
364
|
// src/network/ports.ts
|
|
340
365
|
import { execFile as execFile3 } from "child_process";
|
|
366
|
+
import { readdir, readFile, readlink } from "fs/promises";
|
|
341
367
|
import { createConnection, createServer } from "net";
|
|
342
368
|
import { promisify as promisify3 } from "util";
|
|
343
369
|
var execFileAsync3 = promisify3(execFile3);
|
|
@@ -372,11 +398,75 @@ async function findListeningPidsWithLsof(port) {
|
|
|
372
398
|
return [];
|
|
373
399
|
}
|
|
374
400
|
}
|
|
401
|
+
async function findListeningPidsWithProc(port) {
|
|
402
|
+
const inodes = await findListeningSocketInodesWithProc(port);
|
|
403
|
+
if (inodes.size === 0) {
|
|
404
|
+
return [];
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
const entries = await readdir("/proc", { withFileTypes: true });
|
|
408
|
+
const pids = /* @__PURE__ */ new Set();
|
|
409
|
+
for (const entry of entries) {
|
|
410
|
+
if (!entry.isDirectory() || !/^\d+$/.test(entry.name)) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (await processHasSocketInode(entry.name, inodes)) {
|
|
414
|
+
pids.add(Number.parseInt(entry.name, 10));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return [...pids];
|
|
418
|
+
} catch {
|
|
419
|
+
return [];
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
async function findListeningSocketInodesWithProc(port) {
|
|
423
|
+
const inodes = /* @__PURE__ */ new Set();
|
|
424
|
+
await collectListeningSocketInodes("/proc/net/tcp", port, inodes);
|
|
425
|
+
await collectListeningSocketInodes("/proc/net/tcp6", port, inodes);
|
|
426
|
+
return inodes;
|
|
427
|
+
}
|
|
428
|
+
async function collectListeningSocketInodes(path, port, inodes) {
|
|
429
|
+
try {
|
|
430
|
+
const content = await readFile(path, "utf8");
|
|
431
|
+
for (const line of content.split("\n").slice(1)) {
|
|
432
|
+
const fields = line.trim().split(/\s+/);
|
|
433
|
+
const localAddress = fields[1];
|
|
434
|
+
const state = fields[3];
|
|
435
|
+
const inode = fields[9];
|
|
436
|
+
if (localAddress === void 0 || state === void 0 || inode === void 0) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const localPort = Number.parseInt(localAddress.split(":")[1] ?? "", 16);
|
|
440
|
+
if (localPort === port && state === "0A") {
|
|
441
|
+
inodes.add(inode);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
async function processHasSocketInode(pid, inodes) {
|
|
448
|
+
try {
|
|
449
|
+
const descriptors = await readdir(`/proc/${pid}/fd`);
|
|
450
|
+
for (const descriptor of descriptors) {
|
|
451
|
+
try {
|
|
452
|
+
const link = await readlink(`/proc/${pid}/fd/${descriptor}`);
|
|
453
|
+
const match = /^socket:\[(\d+)\]$/.exec(link);
|
|
454
|
+
if (match?.[1] !== void 0 && inodes.has(match[1])) {
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
} catch {
|
|
461
|
+
}
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
375
464
|
async function findListeningPids(port) {
|
|
376
465
|
if (process.platform === "win32") {
|
|
377
466
|
return await findListeningPidsWithNetstat(port);
|
|
378
467
|
}
|
|
379
|
-
|
|
468
|
+
const lsofPids = await findListeningPidsWithLsof(port);
|
|
469
|
+
return lsofPids.length > 0 ? lsofPids : await findListeningPidsWithProc(port);
|
|
380
470
|
}
|
|
381
471
|
async function isPortFree(port) {
|
|
382
472
|
return await new Promise((resolve) => {
|
|
@@ -459,7 +549,7 @@ async function killProcessOnPort(port) {
|
|
|
459
549
|
|
|
460
550
|
// src/session-state/store.ts
|
|
461
551
|
import { randomUUID } from "crypto";
|
|
462
|
-
import { mkdir as mkdir2, readFile, rename, writeFile } from "fs/promises";
|
|
552
|
+
import { mkdir as mkdir2, readFile as readFile2, rename, writeFile } from "fs/promises";
|
|
463
553
|
import { hostname as getHostname } from "os";
|
|
464
554
|
import { dirname as dirname2 } from "path";
|
|
465
555
|
import process2 from "process";
|
|
@@ -516,7 +606,7 @@ async function withFileLock(lockPath, work, options) {
|
|
|
516
606
|
async function readJsonFile(path) {
|
|
517
607
|
let raw;
|
|
518
608
|
try {
|
|
519
|
-
raw = await
|
|
609
|
+
raw = await readFile2(path, "utf8");
|
|
520
610
|
} catch (err) {
|
|
521
611
|
const code = err.code;
|
|
522
612
|
if (code === "ENOENT") {
|