@saptools/cf-debugger 0.1.15 → 0.2.0

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/index.js CHANGED
@@ -1,2358 +1,27 @@
1
- #!/usr/bin/env node
2
-
3
- // src/types.ts
4
- var CfDebuggerError = class extends Error {
5
- code;
6
- stderr;
7
- constructor(code, message, stderr) {
8
- super(message);
9
- this.name = "CfDebuggerError";
10
- this.code = code;
11
- if (stderr !== void 0) {
12
- this.stderr = stderr;
13
- }
14
- }
15
- };
16
-
17
- // src/debug-session/start.ts
18
- import { chmod as chmod3, mkdir as mkdir3, rm as rm2 } from "fs/promises";
19
- import process5 from "process";
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
- cn20: "https://api.cf.cn20.platform.sapcloud.cn",
42
- cn40: "https://api.cf.cn40.platform.sapcloud.cn",
43
- eu01: "https://api.cf.eu01.hana.ondemand.com",
44
- eu02: "https://api.cf.eu02.hana.ondemand.com",
45
- eu10: "https://api.cf.eu10.hana.ondemand.com",
46
- "eu10-002": "https://api.cf.eu10-002.hana.ondemand.com",
47
- "eu10-003": "https://api.cf.eu10-003.hana.ondemand.com",
48
- "eu10-004": "https://api.cf.eu10-004.hana.ondemand.com",
49
- "eu10-005": "https://api.cf.eu10-005.hana.ondemand.com",
50
- eu11: "https://api.cf.eu11.hana.ondemand.com",
51
- eu13: "https://api.cf.eu13.hana.ondemand.com",
52
- eu20: "https://api.cf.eu20.hana.ondemand.com",
53
- "eu20-001": "https://api.cf.eu20-001.hana.ondemand.com",
54
- "eu20-002": "https://api.cf.eu20-002.hana.ondemand.com",
55
- eu22: "https://api.cf.eu22.hana.ondemand.com",
56
- eu30: "https://api.cf.eu30.hana.ondemand.com",
57
- il30: "https://api.cf.il30.hana.ondemand.com",
58
- in30: "https://api.cf.in30.hana.ondemand.com",
59
- jp01: "https://api.cf.jp01.hana.ondemand.com",
60
- jp10: "https://api.cf.jp10.hana.ondemand.com",
61
- jp20: "https://api.cf.jp20.hana.ondemand.com",
62
- jp30: "https://api.cf.jp30.hana.ondemand.com",
63
- jp31: "https://api.cf.jp31.hana.ondemand.com",
64
- sa30: "https://api.cf.sa30.hana.ondemand.com",
65
- sa31: "https://api.cf.sa31.hana.ondemand.com",
66
- uk20: "https://api.cf.uk20.hana.ondemand.com",
67
- us01: "https://api.cf.us01.hana.ondemand.com",
68
- us02: "https://api.cf.us02.hana.ondemand.com",
69
- us10: "https://api.cf.us10.hana.ondemand.com",
70
- "us10-001": "https://api.cf.us10-001.hana.ondemand.com",
71
- "us10-002": "https://api.cf.us10-002.hana.ondemand.com",
72
- us11: "https://api.cf.us11.hana.ondemand.com",
73
- us20: "https://api.cf.us20.hana.ondemand.com",
74
- us21: "https://api.cf.us21.hana.ondemand.com",
75
- us30: "https://api.cf.us30.hana.ondemand.com"
76
- };
77
- function resolveApiEndpoint(regionKey, override) {
78
- if (override !== void 0 && override !== "") {
79
- return override;
80
- }
81
- const endpoint = REGION_API_ENDPOINTS[regionKey];
82
- if (endpoint === void 0) {
83
- throw new Error(
84
- `Unknown region key: ${regionKey}. Pass \`apiEndpoint\` explicitly to override.`
85
- );
86
- }
87
- return endpoint;
88
- }
89
- function listKnownRegionKeys() {
90
- return Object.keys(REGION_API_ENDPOINTS);
91
- }
92
-
93
- // src/cloud-foundry/execute.ts
94
- import { execFile } from "child_process";
95
- import { promisify } from "util";
96
- var execFileAsync = promisify(execFile);
97
- var MAX_BUFFER = 16 * 1024 * 1024;
98
- var DEFAULT_CF_COMMAND_TIMEOUT_MS = 3e5;
99
- var REDACTED_ARG = "<redacted>";
100
- var MAX_RETRIES = 3;
101
- function buildEnv(cfHome) {
102
- return { ...process.env, CF_HOME: cfHome };
103
- }
104
- function resolveBin(context) {
105
- return context.command ?? process.env["CF_DEBUGGER_CF_BIN"] ?? "cf";
106
- }
107
- function sensitiveArgs(args) {
108
- if (args[0] !== "auth") {
109
- return [];
110
- }
111
- return args.slice(1).filter((arg) => arg.length > 0);
112
- }
113
- function redactText(text, values) {
114
- return values.reduce((current, value) => current.split(value).join(REDACTED_ARG), text);
115
- }
116
- function normalizeSensitiveValues(values) {
117
- return [...new Set(values.filter((value) => value.length > 0))].sort((left, right) => right.length - left.length);
118
- }
119
- function waitForRetry(delayMs, signal) {
120
- if (signal?.aborted) {
121
- return Promise.reject(new CfDebuggerError("ABORTED", "Operation aborted by caller"));
122
- }
123
- return new Promise((resolve, reject) => {
124
- const onAbort = () => {
125
- clearTimeout(timer);
126
- reject(new CfDebuggerError("ABORTED", "Operation aborted by caller"));
127
- };
128
- const timer = setTimeout(() => {
129
- signal?.removeEventListener("abort", onAbort);
130
- resolve();
131
- }, delayMs);
132
- signal?.addEventListener("abort", onAbort, { once: true });
133
- });
134
- }
135
- function formatArgsForError(args) {
136
- if (args[0] !== "auth") {
137
- return args.join(" ");
138
- }
139
- return args.map((arg, index) => index === 0 ? arg : REDACTED_ARG).join(" ");
140
- }
141
- function optionalString(value) {
142
- return typeof value === "string" ? value : void 0;
143
- }
144
- function readFailureDetails(error) {
145
- const errorObject = typeof error === "object" && error !== null ? error : void 0;
146
- const field3 = (key) => errorObject === void 0 ? void 0 : Reflect.get(errorObject, key);
147
- return {
148
- code: optionalString(field3("code")),
149
- killed: field3("killed") === true,
150
- message: error instanceof Error ? error.message : String(error),
151
- stderr: optionalString(field3("stderr")),
152
- stdout: optionalString(field3("stdout"))
153
- };
154
- }
155
- function isTransientNetworkError(error) {
156
- if (error.killed && error.code === void 0) {
157
- return true;
158
- }
159
- const code = error.code ?? "";
160
- const networkCodes = ["ETIMEDOUT", "ECONNRESET", "ENOTFOUND", "ECONNREFUSED", "EHOSTUNREACH", "ENETUNREACH"];
161
- if (networkCodes.includes(code)) {
162
- return true;
163
- }
164
- const output = `${error.message} ${error.stderr ?? ""} ${error.stdout ?? ""}`.toLowerCase();
165
- const transientPhrases = [
166
- "error performing request",
167
- "timeout exceeded",
168
- "connection reset by peer",
169
- "service unavailable",
170
- "bad gateway",
171
- "gateway timeout",
172
- "dial tcp",
173
- "i/o timeout"
174
- ];
175
- if (transientPhrases.some((phrase) => output.includes(phrase))) {
176
- return true;
177
- }
178
- const transientRegexes = [/\b502\b/, /\b503\b/, /\b504\b/, /\btimeout\b/];
179
- return transientRegexes.some((regex) => regex.test(output));
180
- }
181
- function resolveRunOptions(args, input) {
182
- const options = typeof input === "number" ? { timeoutMs: input } : input;
183
- return {
184
- env: options.env,
185
- redactionValues: normalizeSensitiveValues([
186
- ...sensitiveArgs(args),
187
- ...options.sensitiveValues ?? []
188
- ]),
189
- timeoutMs: options.timeoutMs ?? DEFAULT_CF_COMMAND_TIMEOUT_MS
190
- };
191
- }
192
- async function executeCfAttempt(args, context, options) {
193
- const { stdout } = await execFileAsync(resolveBin(context), [...args], {
194
- env: { ...buildEnv(context.cfHome), ...options.env },
195
- maxBuffer: MAX_BUFFER,
196
- ...context.signal === void 0 ? {} : { signal: context.signal },
197
- timeout: options.timeoutMs
198
- });
199
- return stdout;
200
- }
201
- function retryDelayMs(failure, attempt) {
202
- if (!isTransientNetworkError(failure) || attempt > MAX_RETRIES) {
203
- return void 0;
204
- }
205
- return Math.min(1e3 * 2 ** (attempt - 1), 1e4);
206
- }
207
- function createCfCliError(args, failure, redactionValues) {
208
- const stderr = redactText(failure.stderr?.trim() ?? "", redactionValues);
209
- const fallbackMessage = redactText(failure.message, redactionValues);
210
- const detail = stderr.length > 0 ? stderr : fallbackMessage;
211
- return new CfDebuggerError(
212
- "CF_CLI_FAILED",
213
- `cf ${formatArgsForError(args)} failed: ${detail}`,
214
- stderr
215
- );
216
- }
217
- async function runCf(args, context, input = {}) {
218
- if (context.signal?.aborted) {
219
- throw new CfDebuggerError("ABORTED", "Operation aborted by caller");
220
- }
221
- const options = resolveRunOptions(args, input);
222
- let attempt = 0;
223
- for (; ; ) {
224
- try {
225
- return await executeCfAttempt(args, context, options);
226
- } catch (err) {
227
- attempt += 1;
228
- const failure = readFailureDetails(err);
229
- if (context.signal?.aborted || failure.code === "ABORT_ERR") {
230
- throw new CfDebuggerError("ABORTED", "Operation aborted by caller");
231
- }
232
- const delayMs = retryDelayMs(failure, attempt);
233
- if (delayMs !== void 0) {
234
- await waitForRetry(delayMs, context.signal);
235
- continue;
236
- }
237
- throw createCfCliError(args, failure, options.redactionValues);
238
- }
239
- }
240
- }
241
-
242
- // src/cloud-foundry/commands.ts
243
- var execFileAsync2 = promisify2(execFile2);
244
- var CF_AUTH_MAX_ATTEMPTS = 3;
245
- var CURRENT_TARGET_TIMEOUT_MS = 3e4;
246
- function rethrowCallerAbort(error) {
247
- if (error instanceof CfDebuggerError && error.code === "ABORTED") {
248
- throw error;
249
- }
250
- }
251
- function waitForAuthRetry(delayMs, signal) {
252
- if (signal?.aborted) {
253
- return Promise.reject(new CfDebuggerError("ABORTED", "Operation aborted by caller"));
254
- }
255
- return new Promise((resolve, reject) => {
256
- const onAbort = () => {
257
- clearTimeout(timer);
258
- reject(new CfDebuggerError("ABORTED", "Operation aborted by caller"));
259
- };
260
- const timer = setTimeout(() => {
261
- signal?.removeEventListener("abort", onAbort);
262
- resolve();
263
- }, delayMs);
264
- signal?.addEventListener("abort", onAbort, { once: true });
265
- });
266
- }
267
- async function cfApi(apiEndpoint, context) {
268
- await runCf(["api", apiEndpoint], context);
269
- }
270
- async function cfAuth(email, password, context) {
271
- let lastError;
272
- for (let attempt = 0; attempt < CF_AUTH_MAX_ATTEMPTS; attempt++) {
273
- try {
274
- await runCf(["auth"], context, {
275
- env: { CF_PASSWORD: password, CF_USERNAME: email },
276
- sensitiveValues: [email, password]
277
- });
278
- return;
279
- } catch (err) {
280
- lastError = err;
281
- if (err instanceof CfDebuggerError && err.code === "ABORTED") {
282
- throw err;
283
- }
284
- if (attempt < CF_AUTH_MAX_ATTEMPTS - 1) {
285
- await waitForAuthRetry(1e3 * (attempt + 1), context.signal);
286
- }
287
- }
288
- }
289
- if (lastError instanceof Error) {
290
- throw lastError;
291
- }
292
- throw new CfDebuggerError("CF_AUTH_FAILED", `cf auth failed: ${String(lastError)}`);
293
- }
294
- async function cfLogin(apiEndpoint, email, password, context) {
295
- try {
296
- await cfApi(apiEndpoint, context);
297
- await cfAuth(email, password, context);
298
- } catch (err) {
299
- rethrowCallerAbort(err);
300
- if (err instanceof CfDebuggerError) {
301
- throw new CfDebuggerError("CF_LOGIN_FAILED", err.message, err.stderr);
302
- }
303
- throw err;
304
- }
305
- }
306
- async function cfTarget(org, space, context) {
307
- try {
308
- await runCf(["target", "-o", org, "-s", space], context);
309
- } catch (err) {
310
- rethrowCallerAbort(err);
311
- if (err instanceof CfDebuggerError) {
312
- throw new CfDebuggerError("CF_TARGET_FAILED", err.message, err.stderr);
313
- }
314
- throw err;
315
- }
316
- }
317
- async function cfSshEnabled(appName, context) {
318
- try {
319
- const stdout = await runCf(["ssh-enabled", appName], context);
320
- return stdout.toLowerCase().includes("ssh support is enabled");
321
- } catch (err) {
322
- rethrowCallerAbort(err);
323
- return false;
324
- }
325
- }
326
- async function cfEnableSsh(appName, context) {
327
- try {
328
- await runCf(["enable-ssh", appName], context);
329
- } catch (err) {
330
- rethrowCallerAbort(err);
331
- if (err instanceof CfDebuggerError) {
332
- throw new CfDebuggerError("SSH_NOT_ENABLED", err.message, err.stderr);
333
- }
334
- throw err;
335
- }
336
- }
337
- async function cfRestartApp(appName, context) {
338
- await runCf(["restart", appName], context);
339
- }
340
- async function readCurrentCfTarget(options = {}) {
341
- try {
342
- const { stdout } = await execFileAsync2(options.command ?? process.env["CF_DEBUGGER_CF_BIN"] ?? "cf", ["target"], {
343
- env: options.env ?? process.env,
344
- maxBuffer: 16 * 1024 * 1024,
345
- timeout: options.timeoutMs ?? CURRENT_TARGET_TIMEOUT_MS
346
- });
347
- return parseCurrentCfTarget(stdout);
348
- } catch (error) {
349
- const message = error instanceof Error ? error.message : String(error);
350
- throw new CfDebuggerError("CF_TARGET_FAILED", `cf target failed: ${message}`);
351
- }
352
- }
353
- function parseCurrentCfTarget(stdout) {
354
- const fields = parseTargetFields(stdout);
355
- const apiEndpoint = fields.get("api endpoint");
356
- const org = fields.get("org");
357
- const space = fields.get("space");
358
- if (!isPresent(apiEndpoint) || !isPresent(org) || !isPresent(space)) {
359
- return void 0;
360
- }
361
- const region = regionKeyForApiEndpoint(apiEndpoint);
362
- return {
363
- apiEndpoint,
364
- ...region === void 0 ? {} : { region },
365
- org,
366
- space
367
- };
368
- }
369
- function requireCurrentCfRegion(target, instruction = "Pass --region explicitly.") {
370
- if (target.region !== void 0) {
371
- return target.region;
372
- }
373
- throw new CfDebuggerError(
374
- "CF_TARGET_FAILED",
375
- `Current CF API endpoint "${target.apiEndpoint}" does not match a known SAP region. ${instruction}`
376
- );
377
- }
378
- function parseTargetFields(stdout) {
379
- return new Map(
380
- stdout.split("\n").map((line) => {
381
- const separator = line.indexOf(":");
382
- if (separator < 0) {
383
- return void 0;
384
- }
385
- return [
386
- line.slice(0, separator).trim().toLowerCase(),
387
- line.slice(separator + 1).trim()
388
- ];
389
- }).filter((field3) => field3 !== void 0)
390
- );
391
- }
392
- function regionKeyForApiEndpoint(apiEndpoint) {
393
- const normalized = normalizeApiEndpoint(apiEndpoint);
394
- const known = listKnownRegionKeys().find((key) => normalizeApiEndpoint(resolveApiEndpoint(key)) === normalized);
395
- if (known !== void 0) {
396
- return known;
397
- }
398
- return regionKeyFromSapApiEndpoint(normalized);
399
- }
400
- function normalizeApiEndpoint(apiEndpoint) {
401
- return apiEndpoint.trim().replace(/\/+$/, "").toLowerCase();
402
- }
403
- function regionKeyFromSapApiEndpoint(apiEndpoint) {
404
- const match = /^https:\/\/api\.cf\.([a-z]{2}\d{2}(?:-\d{3})?)\.(?:hana\.ondemand\.com|platform\.sapcloud\.cn)$/.exec(apiEndpoint);
405
- return match?.[1];
406
- }
407
- function isPresent(value) {
408
- return value !== void 0 && value.length > 0;
409
- }
410
-
411
- // src/cloud-foundry/ssh.ts
412
- import { spawn } from "child_process";
413
-
414
- // src/cloud-foundry/node-process.ts
415
- var DEFAULT_CF_PROCESS = "web";
416
- var DEFAULT_CF_INSTANCE = 0;
417
- var MAX_MARKER_BYTES = 65536;
418
- var PID_LIST_PATTERN = /^\d+(?:,\d+)*$/;
419
- var PID_PATTERN = /^\d+$/;
420
- function validateProcessName(processName) {
421
- if (processName.length === 0 || processName.startsWith("-") || hasControlCharacter(processName)) {
422
- throw new CfDebuggerError(
423
- "UNSAFE_INPUT",
424
- "process must be non-empty, must not start with a hyphen, and must contain no control characters."
425
- );
426
- }
427
- }
428
- function validateInstance(instance) {
429
- if (!Number.isSafeInteger(instance) || instance < 0) {
430
- throw new CfDebuggerError("UNSAFE_INPUT", "instance must be a non-negative safe integer.");
431
- }
432
- }
433
- function validateNodePid(nodePid) {
434
- if (nodePid !== void 0 && (!Number.isSafeInteger(nodePid) || nodePid <= 0)) {
435
- throw new CfDebuggerError("UNSAFE_INPUT", "nodePid must be a positive safe integer.");
436
- }
437
- }
438
- function resolveNodeTarget(input) {
439
- const processName = (input.process ?? DEFAULT_CF_PROCESS).trim();
440
- const instance = input.instance ?? DEFAULT_CF_INSTANCE;
441
- validateProcessName(processName);
442
- validateInstance(instance);
443
- validateNodePid(input.nodePid);
444
- return input.nodePid === void 0 ? { process: processName, instance } : { process: processName, instance, nodePid: input.nodePid };
445
- }
446
- function hasControlCharacter(value) {
447
- for (let index = 0; index < value.length; index += 1) {
448
- const code = value.charCodeAt(index);
449
- if (code < 32 || code === 127) {
450
- return true;
451
- }
452
- }
453
- return false;
454
- }
455
- function buildNodeInspectorCommand(nodePid) {
456
- if (nodePid !== void 0 && (!Number.isSafeInteger(nodePid) || nodePid <= 0)) {
457
- throw new CfDebuggerError("UNSAFE_INPUT", "nodePid must be a positive safe integer.");
458
- }
459
- return NODE_INSPECTOR_SCRIPT.replace("__REQUESTED_NODE_PID__", nodePid?.toString() ?? "");
460
- }
461
- function parseNodeInspectorMarkers(stdout) {
462
- if (Buffer.byteLength(stdout, "utf8") > MAX_MARKER_BYTES) {
463
- throw new CfDebuggerError("INSPECTOR_OUTPUT_TOO_LARGE", "Inspector startup output exceeded 65536 bytes.");
464
- }
465
- const markers = parseMarkers(stdout);
466
- throwForFailureMarker(markers);
467
- const remoteNodePid = readMarkerPid(markers, "saptools-inspector-node-pid");
468
- const ownerPid = readMarkerPid(markers, "saptools-inspector-owner-pid");
469
- if (!markers.has("saptools-inspector-ready") || remoteNodePid === void 0 || ownerPid !== remoteNodePid) {
470
- throw new CfDebuggerError("INSPECTOR_NOT_READY", "Remote Node inspector did not report a verified owner.");
471
- }
472
- return { remoteNodePid };
473
- }
474
- function parseMarkers(stdout) {
475
- const markers = /* @__PURE__ */ new Map();
476
- for (const rawLine of stdout.split(/\r?\n/)) {
477
- const line = rawLine.trim();
478
- if (!line.startsWith("saptools-inspector-")) {
479
- continue;
480
- }
481
- const separator = line.indexOf("=");
482
- markers.set(separator < 0 ? line : line.slice(0, separator), separator < 0 ? "" : line.slice(separator + 1));
483
- }
484
- return markers;
485
- }
486
- function throwForFailureMarker(markers) {
487
- if (markers.has("saptools-inspector-node-not-found")) {
488
- throw new CfDebuggerError("NODE_PROCESS_NOT_FOUND", "No Node.js process was found in the selected CF instance.");
489
- }
490
- const ambiguous = markers.get("saptools-inspector-node-ambiguous");
491
- if (ambiguous !== void 0) {
492
- const candidates = PID_LIST_PATTERN.test(ambiguous) ? ambiguous.split(",").join(", ") : "unknown";
493
- throw new CfDebuggerError("NODE_PROCESS_AMBIGUOUS", `Multiple Node.js processes were found: ${candidates}. Pass nodePid explicitly.`);
494
- }
495
- const invalid = markers.get("saptools-inspector-node-invalid");
496
- if (invalid !== void 0) {
497
- throw new CfDebuggerError("NODE_PID_INVALID", `Remote PID ${safePidText(invalid)} is not a Node.js process.`);
498
- }
499
- throwForRuntimeFailure(markers);
500
- }
501
- function throwForRuntimeFailure(markers) {
502
- const mismatch = markers.get("saptools-inspector-owner-mismatch");
503
- if (mismatch !== void 0) {
504
- const [selected = "unknown", owner = "unknown"] = mismatch.split(":", 2).map(safePidText);
505
- throw new CfDebuggerError("INSPECTOR_OWNER_MISMATCH", `Selected Node PID ${selected}, but inspector port 9229 is owned by PID ${owner}.`);
506
- }
507
- const signalFailed = markers.get("saptools-inspector-signal-failed");
508
- if (signalFailed !== void 0) {
509
- throw new CfDebuggerError("USR1_SIGNAL_FAILED", `Failed to signal remote Node PID ${safePidText(signalFailed)}.`);
510
- }
511
- if (markers.has("saptools-inspector-not-ready")) {
512
- throw new CfDebuggerError("INSPECTOR_NOT_READY", "Remote Node inspector did not become ready on port 9229.");
513
- }
514
- }
515
- function readMarkerPid(markers, name) {
516
- const raw = markers.get(name);
517
- if (raw === void 0 || !PID_PATTERN.test(raw)) {
518
- return void 0;
519
- }
520
- const value = Number.parseInt(raw, 10);
521
- return Number.isSafeInteger(value) && value > 0 ? value : void 0;
522
- }
523
- function safePidText(raw) {
524
- return PID_PATTERN.test(raw) ? raw : "unknown";
525
- }
526
- var NODE_INSPECTOR_SCRIPT = [
527
- "requested_node_pid=__REQUESTED_NODE_PID__",
528
- "is_node_pid() {",
529
- ' candidate_pid="$1"',
530
- ' candidate_exe="$(readlink "/proc/$candidate_pid/exe" 2>/dev/null || true)"',
531
- ' [ "${candidate_exe##*/}" = node ] || [ "${candidate_exe##*/}" = nodejs ]',
532
- "}",
533
- "find_inspector_owner() {",
534
- ` socket_inode="$(awk '$2 ~ /:240D$/ && $4 == "0A" { print $10; exit }' /proc/net/tcp /proc/net/tcp6 2>/dev/null)"`,
535
- ' [ -n "$socket_inode" ] || return 0',
536
- " for pid_dir in /proc/[0-9]*; do",
537
- ' [ -d "$pid_dir/fd" ] || continue',
538
- ' for fd_path in "$pid_dir"/fd/*; do',
539
- ' fd_target="$(readlink "$fd_path" 2>/dev/null || true)"',
540
- ' if [ "$fd_target" = "socket:[$socket_inode]" ]; then',
541
- ` printf '%s' "\${pid_dir##*/}"`,
542
- " return 0",
543
- " fi",
544
- " done",
545
- " done",
546
- "}",
547
- 'owner_pid="$(find_inspector_owner)"',
548
- 'selected_pid=""',
549
- 'if [ -n "$requested_node_pid" ]; then',
550
- ' if ! is_node_pid "$requested_node_pid"; then',
551
- ' echo "saptools-inspector-node-invalid=$requested_node_pid"',
552
- " exit 0",
553
- " fi",
554
- ' selected_pid="$requested_node_pid"',
555
- 'elif [ -n "$owner_pid" ] && is_node_pid "$owner_pid"; then',
556
- ' selected_pid="$owner_pid"',
557
- "else",
558
- ' candidate_pids=""',
559
- " candidate_count=0",
560
- " for pid_dir in /proc/[0-9]*; do",
561
- ' candidate_pid="${pid_dir##*/}"',
562
- ' is_node_pid "$candidate_pid" || continue',
563
- " candidate_count=$((candidate_count + 1))",
564
- ' candidate_pids="${candidate_pids}${candidate_pids:+,}$candidate_pid"',
565
- ' selected_pid="$candidate_pid"',
566
- " done",
567
- ' if [ "$candidate_count" -eq 0 ]; then echo saptools-inspector-node-not-found; exit 0; fi',
568
- ' if [ "$candidate_count" -ne 1 ]; then echo "saptools-inspector-node-ambiguous=$candidate_pids"; exit 0; fi',
569
- "fi",
570
- 'if [ -n "$owner_pid" ]; then',
571
- ' if [ "$owner_pid" != "$selected_pid" ]; then echo "saptools-inspector-owner-mismatch=$selected_pid:$owner_pid"; exit 0; fi',
572
- ' echo "saptools-inspector-node-pid=$selected_pid"',
573
- ' echo "saptools-inspector-owner-pid=$owner_pid"',
574
- " echo saptools-inspector-ready",
575
- " exit 0",
576
- "fi",
577
- 'if ! kill -USR1 "$selected_pid" 2>/dev/null; then echo "saptools-inspector-signal-failed=$selected_pid"; exit 0; fi',
578
- "attempt=0",
579
- 'while [ "$attempt" -lt 20 ]; do',
580
- ' owner_pid="$(find_inspector_owner)"',
581
- ' if [ -n "$owner_pid" ]; then',
582
- ' if [ "$owner_pid" != "$selected_pid" ]; then echo "saptools-inspector-owner-mismatch=$selected_pid:$owner_pid"; exit 0; fi',
583
- ' echo "saptools-inspector-node-pid=$selected_pid"',
584
- ' echo "saptools-inspector-owner-pid=$owner_pid"',
585
- " echo saptools-inspector-ready",
586
- " exit 0",
587
- " fi",
588
- " attempt=$((attempt + 1))",
589
- " sleep 0.25",
590
- "done",
591
- 'echo "saptools-inspector-not-ready=$selected_pid"'
592
- ].join("\n");
593
-
594
- // src/cloud-foundry/ssh.ts
595
- var DEFAULT_MAX_OUTPUT_BYTES = 65536;
596
- function buildCfSshArgs(appName, target, tail) {
597
- const resolved = resolveNodeTarget(target);
598
- const processArgs = resolved.process === DEFAULT_CF_PROCESS ? [] : ["--process", resolved.process];
599
- return [
600
- "ssh",
601
- appName,
602
- ...processArgs,
603
- "-i",
604
- resolved.instance.toString(),
605
- ...tail
606
- ];
607
- }
608
- async function cfSshOneShot(appName, command, context, rawOptions = DEFAULT_CF_COMMAND_TIMEOUT_MS) {
609
- const options = resolveSshOptions(rawOptions);
610
- const args = buildCfSshArgs(appName, options.target, [
611
- "--disable-pseudo-tty",
612
- "-c",
613
- command
614
- ]);
615
- return await runSshOneShot(args, context, options);
616
- }
617
- function resolveSshOptions(raw) {
618
- const input = typeof raw === "number" ? { timeoutMs: raw } : raw;
619
- const timeoutMs = input.timeoutMs ?? DEFAULT_CF_COMMAND_TIMEOUT_MS;
620
- const maxOutputBytes = input.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
621
- if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
622
- throw new RangeError("timeoutMs must be a positive safe integer");
623
- }
624
- if (!Number.isSafeInteger(maxOutputBytes) || maxOutputBytes <= 0) {
625
- throw new RangeError("maxOutputBytes must be a positive safe integer");
626
- }
627
- return { target: input, timeoutMs, maxOutputBytes };
628
- }
629
- function createBoundedOutput() {
630
- return { chunks: [], bytes: 0, truncated: false };
631
- }
632
- function appendBounded(output, data, limit) {
633
- const incoming = Buffer.isBuffer(data) ? data : Buffer.from(data);
634
- const remaining = Math.max(0, limit - output.bytes);
635
- if (incoming.byteLength > remaining) {
636
- output.truncated = true;
637
- }
638
- if (remaining === 0) {
639
- return;
640
- }
641
- const next = incoming.subarray(0, remaining);
642
- output.chunks.push(next);
643
- output.bytes += next.byteLength;
644
- }
645
- function outputText(output) {
646
- return Buffer.concat(output.chunks, output.bytes).toString("utf8");
647
- }
648
- function createResult(exitCode, stdout, stderr) {
649
- return {
650
- exitCode,
651
- stdout: outputText(stdout),
652
- stderr: outputText(stderr),
653
- outputTruncated: stdout.truncated || stderr.truncated
654
- };
655
- }
656
- function createSshExecutionState() {
657
- return {
658
- stdout: createBoundedOutput(),
659
- stderr: createBoundedOutput(),
660
- settled: false,
661
- aborted: false,
662
- timedOut: false
663
- };
664
- }
665
- function terminateSshExecution(child, state) {
666
- signalChild(child, "SIGTERM");
667
- state.forceKillTimer ??= setTimeout(() => {
668
- signalChild(child, "SIGKILL");
669
- }, 1e3);
670
- }
671
- function createSshSettler(state, options, signal, onAbort, resolve, reject) {
672
- return (result) => {
673
- if (state.settled) {
674
- return;
675
- }
676
- state.settled = true;
677
- if (state.timeoutTimer !== void 0) {
678
- clearTimeout(state.timeoutTimer);
679
- }
680
- if (state.forceKillTimer !== void 0) {
681
- clearTimeout(state.forceKillTimer);
682
- }
683
- signal?.removeEventListener("abort", onAbort);
684
- if (state.aborted) {
685
- reject(new CfDebuggerError("ABORTED", "Operation aborted by caller"));
686
- return;
687
- }
688
- resolve(state.timedOut ? { ...result, timedOutAfterMs: options.timeoutMs } : result);
689
- };
690
- }
691
- function attachSshExecution(child, context, options, resolve, reject) {
692
- const state = createSshExecutionState();
693
- const onAbort = () => {
694
- state.aborted = true;
695
- terminateSshExecution(child, state);
696
- };
697
- const settle = createSshSettler(state, options, context.signal, onAbort, resolve, reject);
698
- state.timeoutTimer = setTimeout(() => {
699
- state.timedOut = true;
700
- terminateSshExecution(child, state);
701
- }, options.timeoutMs);
702
- if (context.signal?.aborted) {
703
- onAbort();
704
- } else {
705
- context.signal?.addEventListener("abort", onAbort, { once: true });
706
- }
707
- child.stdout.on("data", (data) => {
708
- appendBounded(state.stdout, data, options.maxOutputBytes);
709
- });
710
- child.stderr.on("data", (data) => {
711
- appendBounded(state.stderr, data, options.maxOutputBytes);
712
- });
713
- child.on("close", (code, signal) => {
714
- const base = createResult(code, state.stdout, state.stderr);
715
- settle(state.timedOut || signal === null ? base : { ...base, signal });
716
- });
717
- child.on("error", (error) => {
718
- appendBounded(state.stderr, error.message, options.maxOutputBytes);
719
- settle(createResult(null, state.stdout, state.stderr));
720
- });
721
- }
722
- function runSshOneShot(args, context, options) {
723
- if (context.signal?.aborted) {
724
- return Promise.reject(new CfDebuggerError("ABORTED", "Operation aborted by caller"));
725
- }
726
- return new Promise((resolve, reject) => {
727
- const child = spawn(resolveBin(context), [...args], {
728
- env: buildEnv(context.cfHome),
729
- detached: process.platform !== "win32",
730
- stdio: ["ignore", "pipe", "pipe"]
731
- });
732
- attachSshExecution(child, context, options, resolve, reject);
733
- });
734
- }
735
- function signalChild(child, signal) {
736
- if (process.platform !== "win32" && child.pid !== void 0) {
737
- try {
738
- process.kill(-child.pid, signal);
739
- return;
740
- } catch {
741
- }
742
- }
743
- try {
744
- child.kill(signal);
745
- } catch {
746
- }
747
- }
748
- function isSshDisabledError(stderr) {
749
- const lower = stderr.toLowerCase();
750
- return lower.includes("not authorized") || lower.includes("ssh support is disabled");
751
- }
752
- function spawnSshTunnel(appName, localPort, remotePort, context, target = {}) {
753
- const tunnelArg = `${localPort.toString()}:localhost:${remotePort.toString()}`;
754
- const args = buildCfSshArgs(appName, target, ["-N", "-L", tunnelArg]);
755
- const child = spawn(resolveBin(context), [...args], {
756
- env: buildEnv(context.cfHome),
757
- detached: process.platform !== "win32",
758
- stdio: ["ignore", "pipe", "pipe"]
759
- });
760
- child.stdout.resume();
761
- child.stderr.resume();
762
- return child;
763
- }
764
-
765
- // src/paths.ts
766
- import { homedir } from "os";
767
- import { isAbsolute, join } from "path";
768
- var SAPTOOLS_DIR_NAME = ".saptools";
769
- var CF_DEBUGGER_STATE_FILENAME = "cf-debugger-state-v2.json";
770
- var CF_DEBUGGER_LOCK_FILENAME = "cf-debugger-state-v2.lock";
771
- var CF_DEBUGGER_HOMES_DIRNAME = "cf-debugger-homes-v2";
772
- var SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
773
- function saptoolsDir() {
774
- return join(homedir(), SAPTOOLS_DIR_NAME);
775
- }
776
- function stateFilePath() {
777
- return join(saptoolsDir(), CF_DEBUGGER_STATE_FILENAME);
778
- }
779
- function stateLockPath() {
780
- return join(saptoolsDir(), CF_DEBUGGER_LOCK_FILENAME);
781
- }
782
- function isSafeSessionId(sessionId) {
783
- return SESSION_ID_PATTERN.test(sessionId);
784
- }
785
- function sessionCfHomeDir(sessionId) {
786
- if (!isSafeSessionId(sessionId)) {
787
- throw new Error("Invalid debugger session ID.");
788
- }
789
- return join(saptoolsDir(), CF_DEBUGGER_HOMES_DIRNAME, sessionId);
790
- }
791
- function isOwnedSessionCfHomeDir(sessionId, candidate) {
792
- if (!isSafeSessionId(sessionId) || !isAbsolute(candidate)) {
793
- return false;
794
- }
795
- return candidate === sessionCfHomeDir(sessionId);
796
- }
797
-
798
- // src/network/ports.ts
799
- import { execFile as execFile3 } from "child_process";
800
- import { readdir, readFile, readlink } from "fs/promises";
801
- import { createConnection, createServer } from "net";
802
- import { promisify as promisify3 } from "util";
803
- var execFileAsync3 = promisify3(execFile3);
804
- async function findListeningPidsWithNetstat(port) {
805
- try {
806
- const { stdout } = await execFileAsync3("netstat", ["-ano"]);
807
- const pids = /* @__PURE__ */ new Set();
808
- for (const line of stdout.split("\n")) {
809
- if (!line.includes(`:${port.toString()}`) || !line.includes("LISTENING")) {
810
- continue;
811
- }
812
- const parts = line.trim().split(/\s+/);
813
- const last = parts[parts.length - 1];
814
- if (last === void 0) {
815
- continue;
816
- }
817
- const pid = Number.parseInt(last, 10);
818
- if (!Number.isNaN(pid)) {
819
- pids.add(pid);
820
- }
821
- }
822
- return [...pids];
823
- } catch {
824
- return [];
825
- }
826
- }
827
- async function findListeningPidsWithLsof(port) {
828
- try {
829
- const { stdout } = await execFileAsync3("lsof", ["-nP", "-t", "-i", `tcp:${port.toString()}`, "-sTCP:LISTEN"]);
830
- return stdout.trim().split("\n").filter((line) => line.length > 0).map((line) => Number.parseInt(line, 10)).filter((pid) => !Number.isNaN(pid));
831
- } catch {
832
- return [];
833
- }
834
- }
835
- async function findListeningPidsWithProc(port) {
836
- const inodes = await findListeningSocketInodesWithProc(port);
837
- if (inodes.size === 0) {
838
- return [];
839
- }
840
- try {
841
- const entries = await readdir("/proc", { withFileTypes: true });
842
- const pids = /* @__PURE__ */ new Set();
843
- for (const entry of entries) {
844
- if (!entry.isDirectory() || !/^\d+$/.test(entry.name)) {
845
- continue;
846
- }
847
- if (await processHasSocketInode(entry.name, inodes)) {
848
- pids.add(Number.parseInt(entry.name, 10));
849
- }
850
- }
851
- return [...pids];
852
- } catch {
853
- return [];
854
- }
855
- }
856
- async function findListeningSocketInodesWithProc(port) {
857
- const inodes = /* @__PURE__ */ new Set();
858
- await collectListeningSocketInodes("/proc/net/tcp", port, inodes);
859
- await collectListeningSocketInodes("/proc/net/tcp6", port, inodes);
860
- return inodes;
861
- }
862
- async function collectListeningSocketInodes(path, port, inodes) {
863
- try {
864
- const content = await readFile(path, "utf8");
865
- for (const line of content.split("\n").slice(1)) {
866
- const fields = line.trim().split(/\s+/);
867
- const localAddress = fields[1];
868
- const state = fields[3];
869
- const inode = fields[9];
870
- if (localAddress === void 0 || state === void 0 || inode === void 0) {
871
- continue;
872
- }
873
- const localPort = Number.parseInt(localAddress.split(":")[1] ?? "", 16);
874
- if (localPort === port && state === "0A") {
875
- inodes.add(inode);
876
- }
877
- }
878
- } catch {
879
- }
880
- }
881
- async function processHasSocketInode(pid, inodes) {
882
- try {
883
- const descriptors = await readdir(`/proc/${pid}/fd`);
884
- for (const descriptor of descriptors) {
885
- try {
886
- const link = await readlink(`/proc/${pid}/fd/${descriptor}`);
887
- const match = /^socket:\[(\d+)\]$/.exec(link);
888
- if (match?.[1] !== void 0 && inodes.has(match[1])) {
889
- return true;
890
- }
891
- } catch {
892
- }
893
- }
894
- } catch {
895
- }
896
- return false;
897
- }
898
- async function findListeningPids(port) {
899
- if (process.platform === "win32") {
900
- return await findListeningPidsWithNetstat(port);
901
- }
902
- const lsofPids = await findListeningPidsWithLsof(port);
903
- return lsofPids.length > 0 ? lsofPids : await findListeningPidsWithProc(port);
904
- }
905
- async function isPortFree(port) {
906
- return await new Promise((resolve) => {
907
- const server = createServer();
908
- server.once("error", () => {
909
- resolve(false);
910
- });
911
- server.once("listening", () => {
912
- server.close(() => {
913
- resolve(true);
914
- });
915
- });
916
- server.listen(port, "127.0.0.1");
917
- });
918
- }
919
- async function isPortListening(port, timeoutMs = 200) {
920
- return await new Promise((resolve) => {
921
- const socket = createConnection({ port, host: "127.0.0.1" });
922
- socket.setTimeout(timeoutMs);
923
- socket.once("connect", () => {
924
- socket.destroy();
925
- resolve(true);
926
- });
927
- socket.once("error", () => {
928
- socket.destroy();
929
- resolve(false);
930
- });
931
- socket.once("timeout", () => {
932
- socket.destroy();
933
- resolve(false);
934
- });
935
- });
936
- }
937
- function throwIfAborted(signal) {
938
- if (signal?.aborted) {
939
- throw new CfDebuggerError("ABORTED", "Operation aborted by caller");
940
- }
941
- }
942
- function waitForNextProbe(delayMs, signal) {
943
- throwIfAborted(signal);
944
- return new Promise((resolve, reject) => {
945
- const onAbort = () => {
946
- clearTimeout(timer);
947
- reject(new CfDebuggerError("ABORTED", "Operation aborted by caller"));
948
- };
949
- const timer = setTimeout(() => {
950
- signal?.removeEventListener("abort", onAbort);
951
- resolve();
952
- }, delayMs);
953
- signal?.addEventListener("abort", onAbort, { once: true });
954
- });
955
- }
956
- async function probeTunnelReady(port, timeoutMs, signal) {
957
- const pollIntervalMs = 250;
958
- const started = Date.now();
959
- throwIfAborted(signal);
960
- while (Date.now() - started < timeoutMs) {
961
- const connected = await isPortListening(port);
962
- if (connected) {
963
- return true;
964
- }
965
- const remainingMs = timeoutMs - (Date.now() - started);
966
- await waitForNextProbe(Math.min(pollIntervalMs, Math.max(0, remainingMs)), signal);
967
- }
968
- throwIfAborted(signal);
969
- return false;
970
- }
971
- async function findListeningProcessId(port) {
972
- const pids = await findListeningPids(port);
973
- return pids[0];
974
- }
975
-
976
- // src/session-state/store.ts
977
- import { randomUUID as randomUUID2 } from "crypto";
978
- import { chmod as chmod2, mkdir as mkdir2, readFile as readFile3, rename, unlink as unlink2, writeFile } from "fs/promises";
979
- import { hostname as getHostname } from "os";
980
- import { dirname as dirname2, isAbsolute as isAbsolute3 } from "path";
981
- import process3 from "process";
982
-
983
- // src/lock.ts
984
- import { randomUUID } from "crypto";
985
- import { chmod, mkdir, open, readFile as readFile2, stat, unlink } from "fs/promises";
986
- import { hostname } from "os";
987
- import { dirname } from "path";
988
- import process2 from "process";
989
- var DEFAULT_POLL_MS = 50;
990
- var DEFAULT_STALE_MS = 6e4;
991
- var DEFAULT_TIMEOUT_MS = 1e4;
992
- function sleep(ms) {
993
- return new Promise((resolve) => {
994
- setTimeout(resolve, ms);
995
- });
996
- }
997
- function errorCode(error) {
998
- if (typeof error !== "object" || error === null) {
999
- return void 0;
1000
- }
1001
- const code = Reflect.get(error, "code");
1002
- return typeof code === "string" ? code : void 0;
1003
- }
1004
- function field(value, key) {
1005
- return Reflect.get(value, key);
1006
- }
1007
- function parseLockOwner(raw) {
1008
- let value;
1009
- try {
1010
- value = JSON.parse(raw);
1011
- } catch {
1012
- return void 0;
1013
- }
1014
- if (typeof value !== "object" || value === null) {
1015
- return void 0;
1016
- }
1017
- const lockHostname = field(value, "hostname");
1018
- const pid = field(value, "pid");
1019
- const token = field(value, "token");
1020
- const version = field(value, "version");
1021
- if (typeof lockHostname !== "string" || typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0) {
1022
- return void 0;
1023
- }
1024
- if (typeof token !== "string" || version !== "1") {
1025
- return void 0;
1026
- }
1027
- return { hostname: lockHostname, pid, token, version };
1028
- }
1029
- function isProcessAlive(pid) {
1030
- try {
1031
- process2.kill(pid, 0);
1032
- return true;
1033
- } catch (error) {
1034
- return errorCode(error) !== "ESRCH";
1035
- }
1036
- }
1037
- async function readLockOwner(lockPath) {
1038
- try {
1039
- await chmod(lockPath, 384);
1040
- return parseLockOwner(await readFile2(lockPath, "utf8"));
1041
- } catch (error) {
1042
- if (errorCode(error) === "ENOENT") {
1043
- return void 0;
1044
- }
1045
- throw error;
1046
- }
1047
- }
1048
- async function isStaleLock(lockPath, staleMs) {
1049
- let modifiedAt;
1050
- try {
1051
- modifiedAt = (await stat(lockPath)).mtimeMs;
1052
- } catch (error) {
1053
- if (errorCode(error) === "ENOENT") {
1054
- return true;
1055
- }
1056
- throw error;
1057
- }
1058
- const owner = await readLockOwner(lockPath);
1059
- if (owner?.hostname === hostname()) {
1060
- return !isProcessAlive(owner.pid);
1061
- }
1062
- return owner === void 0 && Date.now() - modifiedAt > staleMs;
1063
- }
1064
- async function reclaimAbandonedRecoveryLock(recoveryPath, staleMs) {
1065
- if (!await isStaleLock(recoveryPath, staleMs)) {
1066
- return;
1067
- }
1068
- const owner = await readLockOwner(recoveryPath);
1069
- if (owner !== void 0) {
1070
- await removeOwnedLock(recoveryPath, owner.token);
1071
- return;
1072
- }
1073
- await unlink(recoveryPath).catch((error) => {
1074
- if (errorCode(error) !== "ENOENT") {
1075
- throw error;
1076
- }
1077
- });
1078
- }
1079
- async function reclaimStaleLock(lockPath, staleMs) {
1080
- const recoveryPath = `${lockPath}.recovery`;
1081
- let recoveryLock;
1082
- try {
1083
- recoveryLock = await createFileLock(recoveryPath);
1084
- } catch (error) {
1085
- if (errorCode(error) === "EEXIST") {
1086
- await reclaimAbandonedRecoveryLock(recoveryPath, staleMs);
1087
- return false;
1088
- }
1089
- throw error;
1090
- }
1091
- try {
1092
- if (!await isStaleLock(lockPath, staleMs)) {
1093
- return false;
1094
- }
1095
- try {
1096
- await unlink(lockPath);
1097
- } catch (error) {
1098
- if (errorCode(error) !== "ENOENT") {
1099
- throw error;
1100
- }
1101
- }
1102
- return true;
1103
- } finally {
1104
- await releaseFileLock(recoveryPath, recoveryLock);
1105
- }
1106
- }
1107
- async function removeOwnedLock(lockPath, token) {
1108
- const owner = await readLockOwner(lockPath);
1109
- if (owner?.token !== token) {
1110
- return;
1111
- }
1112
- await unlink(lockPath).catch((error) => {
1113
- if (errorCode(error) !== "ENOENT") {
1114
- throw error;
1115
- }
1116
- });
1117
- }
1118
- async function createFileLock(lockPath) {
1119
- const handle = await open(lockPath, "wx", 384);
1120
- const token = randomUUID();
1121
- try {
1122
- await handle.writeFile(`${JSON.stringify({ hostname: hostname(), pid: process2.pid, token, version: "1" })}
1123
- `, "utf8");
1124
- return { handle, token };
1125
- } catch (error) {
1126
- await handle.close();
1127
- await unlink(lockPath).catch(() => false);
1128
- throw error;
1129
- }
1130
- }
1131
- async function acquireFileLock(lockPath, timeoutMs, pollMs, staleMs) {
1132
- const deadline = Date.now() + timeoutMs;
1133
- const parentDir = dirname(lockPath);
1134
- await mkdir(parentDir, { recursive: true, mode: 448 });
1135
- await chmod(parentDir, 448);
1136
- for (; ; ) {
1137
- try {
1138
- return await createFileLock(lockPath);
1139
- } catch (error) {
1140
- if (errorCode(error) !== "EEXIST") {
1141
- throw error;
1142
- }
1143
- }
1144
- if (await reclaimStaleLock(lockPath, staleMs)) {
1145
- continue;
1146
- }
1147
- if (Date.now() > deadline) {
1148
- throw new Error(`Timed out acquiring file lock at ${lockPath}`);
1149
- }
1150
- await sleep(pollMs);
1151
- }
1152
- }
1153
- async function releaseFileLock(lockPath, lock) {
1154
- await lock.handle.close();
1155
- await removeOwnedLock(lockPath, lock.token);
1156
- }
1157
- async function withFileLock(lockPath, work, options) {
1158
- const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1159
- const pollMs = options?.pollMs ?? DEFAULT_POLL_MS;
1160
- const staleMs = options?.staleMs ?? DEFAULT_STALE_MS;
1161
- const lock = await acquireFileLock(lockPath, timeoutMs, pollMs, staleMs);
1162
- try {
1163
- return await work();
1164
- } finally {
1165
- await releaseFileLock(lockPath, lock);
1166
- }
1167
- }
1168
-
1169
- // src/session-state/decoder.ts
1170
- import { isAbsolute as isAbsolute2 } from "path";
1171
- var INVALID_SESSION = new Error("Invalid persisted debugger session");
1172
- var VALID_STATUSES = /* @__PURE__ */ new Set([
1173
- "starting",
1174
- "logging-in",
1175
- "targeting",
1176
- "ssh-enabling",
1177
- "ssh-restarting",
1178
- "signaling",
1179
- "tunneling",
1180
- "ready",
1181
- "stopping",
1182
- "stopped",
1183
- "error"
1184
- ]);
1185
- function field2(value, key) {
1186
- return Reflect.get(value, key);
1187
- }
1188
- function requireString(value, key) {
1189
- const candidate = field2(value, key);
1190
- if (typeof candidate !== "string" || candidate.length === 0) {
1191
- throw INVALID_SESSION;
1192
- }
1193
- return candidate;
1194
- }
1195
- function optionalString2(value, key) {
1196
- const candidate = field2(value, key);
1197
- if (candidate === void 0) {
1198
- return void 0;
1199
- }
1200
- if (typeof candidate !== "string") {
1201
- throw INVALID_SESSION;
1202
- }
1203
- return candidate;
1204
- }
1205
- function requireInteger(value, key, minimum, maximum) {
1206
- const candidate = field2(value, key);
1207
- if (!Number.isSafeInteger(candidate) || typeof candidate !== "number") {
1208
- throw INVALID_SESSION;
1209
- }
1210
- if (candidate < minimum || candidate > maximum) {
1211
- throw INVALID_SESSION;
1212
- }
1213
- return candidate;
1214
- }
1215
- function optionalInteger(value, key, minimum) {
1216
- const candidate = field2(value, key);
1217
- if (candidate === void 0) {
1218
- return void 0;
1219
- }
1220
- if (!Number.isSafeInteger(candidate) || typeof candidate !== "number" || candidate < minimum) {
1221
- throw INVALID_SESSION;
1222
- }
1223
- return candidate;
1224
- }
1225
- function requireSessionId(value) {
1226
- const sessionId = requireString(value, "sessionId");
1227
- if (!isSafeSessionId(sessionId)) {
1228
- throw INVALID_SESSION;
1229
- }
1230
- return sessionId;
1231
- }
1232
- function requireAbsolutePath(value, key) {
1233
- const path = requireString(value, key);
1234
- if (!isAbsolute2(path)) {
1235
- throw INVALID_SESSION;
1236
- }
1237
- return path;
1238
- }
1239
- function requireTimestamp(value) {
1240
- const timestamp = requireString(value, "startedAt");
1241
- if (Number.isNaN(Date.parse(timestamp))) {
1242
- throw INVALID_SESSION;
1243
- }
1244
- return timestamp;
1245
- }
1246
- function optionalTimestamp(value, key) {
1247
- const timestamp = optionalString2(value, key);
1248
- if (timestamp !== void 0 && Number.isNaN(Date.parse(timestamp))) {
1249
- throw INVALID_SESSION;
1250
- }
1251
- return timestamp;
1252
- }
1253
- function isSessionStatus(value) {
1254
- return VALID_STATUSES.has(value);
1255
- }
1256
- function requireStatus(value) {
1257
- const status = requireString(value, "status");
1258
- if (!isSessionStatus(status)) {
1259
- throw INVALID_SESSION;
1260
- }
1261
- return status;
1262
- }
1263
- function decodeSession(value) {
1264
- if (typeof value !== "object" || value === null) {
1265
- return void 0;
1266
- }
1267
- try {
1268
- const processName = requireString(value, "process");
1269
- const instance = requireInteger(value, "instance", 0, Number.MAX_SAFE_INTEGER);
1270
- const nodePid = optionalInteger(value, "nodePid", 1);
1271
- const target = resolveNodeTarget({
1272
- process: processName,
1273
- instance,
1274
- ...nodePid === void 0 ? {} : { nodePid }
1275
- });
1276
- const pid = requireInteger(value, "pid", 1, Number.MAX_SAFE_INTEGER);
1277
- const controllerPid = requireInteger(value, "controllerPid", 1, Number.MAX_SAFE_INTEGER);
1278
- const tunnelPid = optionalInteger(value, "tunnelPid", 1);
1279
- const remoteNodePid = optionalInteger(value, "remoteNodePid", 1);
1280
- const stopRequestedAt = optionalTimestamp(value, "stopRequestedAt");
1281
- const message = optionalString2(value, "message");
1282
- const status = requireStatus(value);
1283
- if (pid !== (tunnelPid ?? controllerPid) || status === "ready" && tunnelPid === void 0) {
1284
- throw INVALID_SESSION;
1285
- }
1286
- return {
1287
- sessionId: requireSessionId(value),
1288
- pid,
1289
- controllerPid,
1290
- ...tunnelPid === void 0 ? {} : { tunnelPid },
1291
- hostname: requireString(value, "hostname"),
1292
- region: requireString(value, "region"),
1293
- org: requireString(value, "org"),
1294
- space: requireString(value, "space"),
1295
- app: requireString(value, "app"),
1296
- process: target.process,
1297
- instance: target.instance,
1298
- ...nodePid === void 0 ? {} : { nodePid },
1299
- apiEndpoint: requireString(value, "apiEndpoint"),
1300
- localPort: requireInteger(value, "localPort", 1, 65535),
1301
- remotePort: requireInteger(value, "remotePort", 1, 65535),
1302
- cfHomeDir: requireAbsolutePath(value, "cfHomeDir"),
1303
- startedAt: requireTimestamp(value),
1304
- status,
1305
- ...remoteNodePid === void 0 ? {} : { remoteNodePid },
1306
- ...stopRequestedAt === void 0 ? {} : { stopRequestedAt },
1307
- ...message === void 0 ? {} : { message }
1308
- };
1309
- } catch {
1310
- return void 0;
1311
- }
1312
- }
1313
- function decodeStateFile(value) {
1314
- if (typeof value !== "object" || value === null || field2(value, "version") !== "2") {
1315
- return void 0;
1316
- }
1317
- const rawSessions = field2(value, "sessions");
1318
- if (!Array.isArray(rawSessions)) {
1319
- return void 0;
1320
- }
1321
- const sessionIds = /* @__PURE__ */ new Set();
1322
- const sessions = [];
1323
- for (const rawSession of rawSessions) {
1324
- const session = decodeSession(rawSession);
1325
- if (session === void 0 || sessionIds.has(session.sessionId)) {
1326
- return void 0;
1327
- }
1328
- sessionIds.add(session.sessionId);
1329
- sessions.push(session);
1330
- }
1331
- return { version: "2", sessions };
1332
- }
1333
-
1334
- // src/session-state/store.ts
1335
- function errorCode2(error) {
1336
- if (typeof error !== "object" || error === null) {
1337
- return void 0;
1338
- }
1339
- const code = Reflect.get(error, "code");
1340
- return typeof code === "string" ? code : void 0;
1341
- }
1342
- async function readJsonFile(path) {
1343
- let raw;
1344
- try {
1345
- await chmod2(path, 384);
1346
- raw = await readFile3(path, "utf8");
1347
- } catch (err) {
1348
- if (errorCode2(err) === "ENOENT") {
1349
- return void 0;
1350
- }
1351
- throw err;
1352
- }
1353
- try {
1354
- return JSON.parse(raw);
1355
- } catch {
1356
- process3.stderr.write(
1357
- `[cf-debugger] warning: state file at ${path} is not valid JSON; resetting to empty.
1358
- `
1359
- );
1360
- return void 0;
1361
- }
1362
- }
1363
- async function writeJsonFileAtomic(path, value) {
1364
- const tempPath = `${path}.${randomUUID2()}.tmp`;
1365
- const parentDir = dirname2(path);
1366
- await mkdir2(parentDir, { recursive: true, mode: 448 });
1367
- await chmod2(parentDir, 448);
1368
- let renamed = false;
1369
- try {
1370
- await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
1371
- `, {
1372
- encoding: "utf8",
1373
- flag: "wx",
1374
- mode: 384
1375
- });
1376
- await rename(tempPath, path);
1377
- renamed = true;
1378
- await chmod2(path, 384);
1379
- } finally {
1380
- if (!renamed) {
1381
- await unlink2(tempPath).catch(() => false);
1382
- }
1383
- }
1384
- }
1385
- function emptyState() {
1386
- return { version: "2", sessions: [] };
1387
- }
1388
- function isPidAlive(pid) {
1389
- try {
1390
- process3.kill(pid, 0);
1391
- return true;
1392
- } catch (err) {
1393
- if (errorCode2(err) === "ESRCH") {
1394
- return false;
1395
- }
1396
- return true;
1397
- }
1398
- }
1399
- function isPidOrGroupAlive(pid) {
1400
- if (isPidAlive(pid)) {
1401
- return true;
1402
- }
1403
- return isProcessGroupAlive(pid);
1404
- }
1405
- function isProcessGroupAlive(pid) {
1406
- return process3.platform !== "win32" && isPidAlive(-pid);
1407
- }
1408
- async function isSessionHealthy(session, host) {
1409
- if (session.hostname !== host) {
1410
- return false;
1411
- }
1412
- if (session.status !== "ready" && isPidAlive(session.controllerPid ?? session.pid)) {
1413
- return true;
1414
- }
1415
- if (session.tunnelPid !== void 0 && isPidOrGroupAlive(session.tunnelPid)) {
1416
- return true;
1417
- }
1418
- return await isPortListening(session.localPort);
1419
- }
1420
- async function filterStaleSessions(sessions) {
1421
- const host = getHostname();
1422
- const checks = await Promise.all(
1423
- sessions.map(async (session) => [
1424
- session,
1425
- await isSessionHealthy(session, host)
1426
- ])
1427
- );
1428
- return checks.filter(([, healthy]) => healthy).map(([session]) => session);
1429
- }
1430
- async function readStateRaw() {
1431
- const path = stateFilePath();
1432
- const parsed = await readJsonFile(path);
1433
- if (parsed === void 0) {
1434
- return emptyState();
1435
- }
1436
- const decoded = decodeStateFile(parsed);
1437
- if (decoded !== void 0) {
1438
- return decoded;
1439
- }
1440
- process3.stderr.write(
1441
- `[cf-debugger] warning: state file at ${path} has an invalid structure; resetting to empty.
1442
- `
1443
- );
1444
- return emptyState();
1445
- }
1446
- async function writeState(state) {
1447
- await writeJsonFileAtomic(stateFilePath(), state);
1448
- }
1449
- async function readAndPruneLocked() {
1450
- const raw = await readStateRaw();
1451
- const host = getHostname();
1452
- const remote = raw.sessions.filter((session) => session.hostname !== host);
1453
- const local = raw.sessions.filter((session) => session.hostname === host);
1454
- const pruned = await filterStaleSessions(local);
1455
- const removed = local.filter(
1456
- (session) => !pruned.some((active) => active.sessionId === session.sessionId)
1457
- );
1458
- if (removed.length > 0) {
1459
- await writeState({ version: "2", sessions: [...remote, ...pruned] });
1460
- }
1461
- return { sessions: pruned, removed };
1462
- }
1463
- async function readActiveSessions() {
1464
- const result = await withFileLock(stateLockPath(), readAndPruneLocked);
1465
- return result.sessions;
1466
- }
1467
- async function readSessionSnapshot() {
1468
- return await withFileLock(stateLockPath(), async () => {
1469
- const raw = await readStateRaw();
1470
- return raw.sessions;
1471
- });
1472
- }
1473
- async function readAndPruneActiveSessions() {
1474
- return await withFileLock(stateLockPath(), readAndPruneLocked);
1475
- }
1476
- function sessionKeyString(key) {
1477
- const base = `${key.region}:${key.org}:${key.space}:${key.app}`;
1478
- if (key.process === void 0 && key.instance === void 0) {
1479
- return base;
1480
- }
1481
- const target = resolveNodeTarget(key);
1482
- return `${base}:${target.process}:${target.instance.toString()}`;
1483
- }
1484
- function matchesKey(session, key) {
1485
- const sessionTarget = resolveNodeTarget(session);
1486
- const keyTarget = resolveNodeTarget(key);
1487
- return session.region === key.region && session.org === key.org && session.space === key.space && session.app === key.app && sessionTarget.process === keyTarget.process && sessionTarget.instance === keyTarget.instance && (key.apiEndpoint === void 0 || session.apiEndpoint === key.apiEndpoint) && (key.nodePid === void 0 || sessionTarget.nodePid === keyTarget.nodePid);
1488
- }
1489
- function matchesRegistrationTarget(session, input, target) {
1490
- return matchesKey(session, {
1491
- region: input.region,
1492
- org: input.org,
1493
- space: input.space,
1494
- app: input.app,
1495
- process: target.process,
1496
- instance: target.instance,
1497
- apiEndpoint: input.apiEndpoint
1498
- });
1499
- }
1500
- var DEFAULT_BASE_PORT = 2e4;
1501
- var DEFAULT_MAX_PORT = 20999;
1502
- async function pickPort(preferred, reserved, probe, basePort, maxPort) {
1503
- const tryOrder = [];
1504
- if (preferred !== void 0) {
1505
- tryOrder.push(preferred);
1506
- }
1507
- for (let port = basePort; port <= maxPort; port++) {
1508
- if (port !== preferred) {
1509
- tryOrder.push(port);
1510
- }
1511
- }
1512
- for (const port of tryOrder) {
1513
- if (reserved.has(port)) {
1514
- continue;
1515
- }
1516
- const free = await probe(port);
1517
- if (free) {
1518
- return port;
1519
- }
1520
- }
1521
- throw new CfDebuggerError(
1522
- "PORT_UNAVAILABLE",
1523
- `No free local port available in range ${basePort.toString()}\u2013${maxPort.toString()}`
1524
- );
1525
- }
1526
- async function registerNewSession(input) {
1527
- const target = resolveNodeTarget(input);
1528
- return await withFileLock(stateLockPath(), async () => {
1529
- const pruneResult = await readAndPruneLocked();
1530
- const persisted = await readStateRaw();
1531
- const host = getHostname();
1532
- const remoteSessions = persisted.sessions.filter((session2) => session2.hostname !== host);
1533
- const existing = pruneResult.sessions.find((session2) => matchesRegistrationTarget(session2, input, target));
1534
- if (existing) {
1535
- return { session: existing, existing };
1536
- }
1537
- const reservedPorts = new Set(pruneResult.sessions.map((session2) => session2.localPort));
1538
- const localPort = await pickPort(
1539
- input.preferredPort,
1540
- reservedPorts,
1541
- input.portProbe,
1542
- input.basePort ?? DEFAULT_BASE_PORT,
1543
- input.maxPort ?? DEFAULT_MAX_PORT
1544
- );
1545
- const session = createRegisteredSession(input, target, localPort);
1546
- const nextSessions = [...remoteSessions, ...pruneResult.sessions, session];
1547
- await writeState({ version: "2", sessions: nextSessions });
1548
- return { session };
1549
- });
1550
- }
1551
- function createRegisteredSession(input, target, localPort) {
1552
- const sessionId = (input.sessionIdFactory ?? randomUUID2)();
1553
- if (!isSafeSessionId(sessionId)) {
1554
- throw new CfDebuggerError("UNSAFE_INPUT", "Generated debugger session ID is invalid.");
1555
- }
1556
- const cfHomeDir = input.cfHomeForSession(sessionId);
1557
- if (!isAbsolute3(cfHomeDir)) {
1558
- throw new CfDebuggerError("UNSAFE_INPUT", "Debugger CF home must be an absolute path.");
1559
- }
1560
- return {
1561
- sessionId,
1562
- pid: process3.pid,
1563
- controllerPid: process3.pid,
1564
- hostname: getHostname(),
1565
- region: input.region,
1566
- org: input.org,
1567
- space: input.space,
1568
- app: input.app,
1569
- process: target.process,
1570
- instance: target.instance,
1571
- ...target.nodePid === void 0 ? {} : { nodePid: target.nodePid },
1572
- apiEndpoint: input.apiEndpoint,
1573
- localPort,
1574
- remotePort: 9229,
1575
- cfHomeDir,
1576
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1577
- status: "starting"
1578
- };
1579
- }
1580
- async function updateSessionStatus(sessionId, status, message) {
1581
- return await withFileLock(stateLockPath(), async () => {
1582
- const raw = await readStateRaw();
1583
- let updated;
1584
- const nextSessions = raw.sessions.map((session) => {
1585
- if (session.sessionId !== sessionId) {
1586
- return session;
1587
- }
1588
- if (status !== "stopping" && startupMutationBlocked(session)) {
1589
- updated = session;
1590
- return session;
1591
- }
1592
- if (status === "ready" && session.tunnelPid === void 0) {
1593
- throw new CfDebuggerError(
1594
- "SESSION_STATE_CONFLICT",
1595
- "A debugger session cannot become ready before its tunnel PID is recorded."
1596
- );
1597
- }
1598
- const base = withoutMessage(session);
1599
- const next = message === void 0 ? { ...base, status } : { ...base, status, message };
1600
- updated = next;
1601
- return next;
1602
- });
1603
- if (updated) {
1604
- await writeState({ version: "2", sessions: nextSessions });
1605
- }
1606
- return updated;
1607
- });
1608
- }
1609
- async function updateSessionPid(sessionId, pid) {
1610
- if (!Number.isSafeInteger(pid) || pid <= 0) {
1611
- throw new CfDebuggerError("UNSAFE_INPUT", "Tunnel PID must be a positive safe integer.");
1612
- }
1613
- return await withFileLock(stateLockPath(), async () => {
1614
- const raw = await readStateRaw();
1615
- let updated;
1616
- const nextSessions = raw.sessions.map((session) => {
1617
- if (session.sessionId !== sessionId) {
1618
- return session;
1619
- }
1620
- if (startupMutationBlocked(session)) {
1621
- updated = session;
1622
- return session;
1623
- }
1624
- const next = { ...session, pid, tunnelPid: pid };
1625
- updated = next;
1626
- return next;
1627
- });
1628
- if (updated !== void 0) {
1629
- await writeState({ version: "2", sessions: nextSessions });
1630
- }
1631
- return updated;
1632
- });
1633
- }
1634
- function withoutMessage(session) {
1635
- const { message, ...clone } = session;
1636
- void message;
1637
- return clone;
1638
- }
1639
- function startupMutationBlocked(session) {
1640
- return session.status === "stopping" || session.stopRequestedAt !== void 0;
1641
- }
1642
- async function updateSessionRemoteNodePid(sessionId, remoteNodePid) {
1643
- resolveNodeTarget({ nodePid: remoteNodePid });
1644
- return await withFileLock(stateLockPath(), async () => {
1645
- const raw = await readStateRaw();
1646
- let updated;
1647
- const nextSessions = raw.sessions.map((session) => {
1648
- if (session.sessionId !== sessionId) {
1649
- return session;
1650
- }
1651
- if (startupMutationBlocked(session)) {
1652
- updated = session;
1653
- return session;
1654
- }
1655
- const next = { ...session, remoteNodePid };
1656
- updated = next;
1657
- return next;
1658
- });
1659
- if (updated !== void 0) {
1660
- await writeState({ version: "2", sessions: nextSessions });
1661
- }
1662
- return updated;
1663
- });
1664
- }
1665
- async function removeSession(sessionId) {
1666
- return await withFileLock(stateLockPath(), async () => {
1667
- const raw = await readStateRaw();
1668
- const target = raw.sessions.find((session) => session.sessionId === sessionId);
1669
- if (!target) {
1670
- return void 0;
1671
- }
1672
- const remaining = raw.sessions.filter((session) => session.sessionId !== sessionId);
1673
- await writeState({ version: "2", sessions: remaining });
1674
- return target;
1675
- });
1676
- }
1677
- async function requestSessionStop(sessionId) {
1678
- return await withFileLock(stateLockPath(), async () => {
1679
- const raw = await readStateRaw();
1680
- const target = raw.sessions.find((session) => session.sessionId === sessionId);
1681
- if (target === void 0) {
1682
- return void 0;
1683
- }
1684
- if (target.status === "ready" || target.stopRequestedAt !== void 0) {
1685
- return { session: target, previousStatus: target.status };
1686
- }
1687
- const requested = { ...target, stopRequestedAt: (/* @__PURE__ */ new Date()).toISOString() };
1688
- const sessions = raw.sessions.map(
1689
- (session) => session.sessionId === sessionId ? requested : session
1690
- );
1691
- await writeState({ version: "2", sessions });
1692
- return { session: requested, previousStatus: target.status };
1693
- });
1694
- }
1695
-
1696
- // src/debug-session/constants.ts
1697
- var DEFAULT_TUNNEL_READY_TIMEOUT_MS = 18e4;
1698
- var POST_USR1_DELAY_MS = 300;
1699
- var CHILD_SIGTERM_GRACE_MS = 2e3;
1700
- var CHILD_SIGKILL_GRACE_MS = 1e3;
1701
- var PID_TERMINATION_POLL_MS = 100;
1702
-
1703
- // src/debug-session/orphans.ts
1704
- import { rm } from "fs/promises";
1705
- import { hostname as getHostname2 } from "os";
1706
- async function pruneAndCleanupOrphans() {
1707
- const result = await readAndPruneActiveSessions();
1708
- const host = getHostname2();
1709
- for (const removed of result.removed) {
1710
- if (removed.hostname === host && isOwnedSessionCfHomeDir(removed.sessionId, removed.cfHomeDir)) {
1711
- try {
1712
- await rm(removed.cfHomeDir, { recursive: true, force: true });
1713
- } catch {
1714
- }
1715
- }
1716
- }
1717
- return result;
1718
- }
1719
-
1720
- // src/debug-session/processes.ts
1721
- import process4 from "process";
1722
- async function terminatePidOrGroup(pid, timeoutMs = CHILD_SIGTERM_GRACE_MS, pinnedTarget) {
1723
- const targetKind = pinnedTarget ?? (isProcessGroupAlive(pid) ? "group" : "pid");
1724
- const targetAlive = () => targetKind === "group" ? isProcessGroupAlive(pid) : isPidAlive(pid);
1725
- if (!targetAlive()) {
1726
- return "terminated";
1727
- }
1728
- try {
1729
- process4.kill(targetKind === "group" ? -pid : pid, "SIGTERM");
1730
- } catch {
1731
- }
1732
- const startedAt = Date.now();
1733
- while (Date.now() - startedAt < timeoutMs) {
1734
- if (!targetAlive()) {
1735
- return "terminated";
1736
- }
1737
- await new Promise((resolve) => {
1738
- setTimeout(resolve, PID_TERMINATION_POLL_MS);
1739
- });
1740
- }
1741
- try {
1742
- process4.kill(targetKind === "group" ? -pid : pid, "SIGKILL");
1743
- } catch {
1744
- }
1745
- const forceStartedAt = Date.now();
1746
- while (Date.now() - forceStartedAt < CHILD_SIGKILL_GRACE_MS) {
1747
- if (!targetAlive()) {
1748
- return "terminated";
1749
- }
1750
- await new Promise((resolve) => {
1751
- setTimeout(resolve, PID_TERMINATION_POLL_MS);
1752
- });
1753
- }
1754
- return targetAlive() ? "still-alive" : "terminated";
1755
- }
1756
- async function killProcessGroupOrProc(child, timeoutMs = CHILD_SIGTERM_GRACE_MS) {
1757
- if (child.pid === void 0) {
1758
- return "terminated";
1759
- }
1760
- const childClosed = child.exitCode !== null || child.signalCode !== null;
1761
- if (childClosed && process4.platform === "win32") {
1762
- return "terminated";
1763
- }
1764
- return await terminatePidOrGroup(child.pid, timeoutMs, childClosed ? "group" : void 0);
1765
- }
1766
-
1767
- // src/debug-session/startup-cancellation.ts
1768
- var STOP_REQUEST_POLL_MS = 50;
1769
- function cancellationRequested(sessions, sessionId) {
1770
- const session = sessions.find((candidate) => candidate.sessionId === sessionId);
1771
- return session === void 0 || session.stopRequestedAt !== void 0;
1772
- }
1773
- function createStartupCancellation(sessionId, callerSignal) {
1774
- const controller = new AbortController();
1775
- let active = true;
1776
- let timer;
1777
- const onCallerAbort = () => {
1778
- controller.abort();
1779
- };
1780
- const schedule = () => {
1781
- timer = setTimeout(() => {
1782
- void poll();
1783
- }, STOP_REQUEST_POLL_MS);
1784
- timer.unref();
1785
- };
1786
- const poll = async () => {
1787
- try {
1788
- const sessions = await readSessionSnapshot();
1789
- if (active && cancellationRequested(sessions, sessionId)) {
1790
- controller.abort();
1791
- }
1792
- } catch {
1793
- }
1794
- if (active && !controller.signal.aborted) {
1795
- schedule();
1796
- }
1797
- };
1798
- callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
1799
- if (callerSignal?.aborted === true) {
1800
- controller.abort();
1801
- } else {
1802
- void poll();
1803
- }
1804
- return {
1805
- signal: controller.signal,
1806
- dispose: () => {
1807
- active = false;
1808
- clearTimeout(timer);
1809
- callerSignal?.removeEventListener("abort", onCallerAbort);
1810
- }
1811
- };
1812
- }
1813
-
1814
- // src/debug-session/start.ts
1815
- function signalFailureDetail(result) {
1816
- if (result.timedOutAfterMs !== void 0) {
1817
- return `timed out after ${(result.timedOutAfterMs / 1e3).toString()}s`;
1818
- }
1819
- const stderr = result.stderr.trim();
1820
- if (stderr.length > 0) {
1821
- return stderr;
1822
- }
1823
- if (result.signal !== void 0) {
1824
- return `terminated by signal ${result.signal}`;
1825
- }
1826
- return `exit code ${String(result.exitCode)}`;
1827
- }
1828
- function checkAbort(signal) {
1829
- if (signal?.aborted) {
1830
- throw new CfDebuggerError("ABORTED", "Operation aborted by caller");
1831
- }
1832
- }
1833
- function requireStartupState(state, expectedStatus) {
1834
- if (state === void 0) {
1835
- throw new CfDebuggerError(
1836
- "SESSION_STATE_LOST",
1837
- "Debugger session ownership state disappeared during startup."
1838
- );
1839
- }
1840
- if (state.stopRequestedAt !== void 0 || state.status === "stopping") {
1841
- throw new CfDebuggerError("ABORTED", "Debugger session stop was requested during startup.");
1842
- }
1843
- if (expectedStatus !== void 0 && state.status !== expectedStatus) {
1844
- throw new CfDebuggerError(
1845
- "SESSION_STATE_CONFLICT",
1846
- `Debugger session state did not transition to ${expectedStatus}.`
1847
- );
1848
- }
1849
- return state;
1850
- }
1851
- async function transitionStartupStatus(sessionId, status, message) {
1852
- return requireStartupState(await updateSessionStatus(sessionId, status, message), status);
1853
- }
1854
- function requireCredentials(options) {
1855
- const email = options.email ?? process5.env["SAP_EMAIL"];
1856
- const password = options.password ?? process5.env["SAP_PASSWORD"];
1857
- if (email === void 0 || email === "") {
1858
- throw new CfDebuggerError(
1859
- "MISSING_CREDENTIALS",
1860
- "SAP email is required. Pass `email` or set SAP_EMAIL env var."
1861
- );
1862
- }
1863
- if (password === void 0 || password === "") {
1864
- throw new CfDebuggerError(
1865
- "MISSING_CREDENTIALS",
1866
- "SAP password is required. Pass `password` or set SAP_PASSWORD env var."
1867
- );
1868
- }
1869
- return { email, password };
1870
- }
1871
- async function registerSession(options, target, apiEndpoint) {
1872
- const registration = await registerNewSession({
1873
- region: options.region,
1874
- org: options.org,
1875
- space: options.space,
1876
- app: options.app,
1877
- process: target.process,
1878
- instance: target.instance,
1879
- ...target.nodePid === void 0 ? {} : { nodePid: target.nodePid },
1880
- apiEndpoint,
1881
- ...options.preferredPort === void 0 ? {} : { preferredPort: options.preferredPort },
1882
- portProbe: isPortFree,
1883
- cfHomeForSession: sessionCfHomeDir
1884
- });
1885
- if (registration.existing) {
1886
- throw new CfDebuggerError(
1887
- "SESSION_ALREADY_RUNNING",
1888
- `A debugger session is already running for ${sessionKeyString(options)} on port ${registration.existing.localPort.toString()} (pid ${registration.existing.pid.toString()}, sessionId ${registration.existing.sessionId}). Stop it first with \`cf-debugger stop\`.`
1889
- );
1890
- }
1891
- return registration.session;
1892
- }
1893
- async function loginAndTarget(options, apiEndpoint, email, password, context, sessionId, emit) {
1894
- emit("logging-in");
1895
- await transitionStartupStatus(sessionId, "logging-in");
1896
- await cfLogin(apiEndpoint, email, password, context);
1897
- checkAbort(context.signal);
1898
- emit("targeting");
1899
- await transitionStartupStatus(sessionId, "targeting");
1900
- await cfTarget(options.org, options.space, context);
1901
- checkAbort(context.signal);
1902
- }
1903
- async function signalRemoteNode(options, target, context, sessionId, emit) {
1904
- emit("signaling");
1905
- await transitionStartupStatus(sessionId, "signaling");
1906
- const signalResult = await executeRemoteSignal(options.app, target, context);
1907
- if (!isSshDisabledError(signalResult.stderr)) {
1908
- return parseSignalResult(options.app, signalResult);
1909
- }
1910
- if (options.allowSshEnableRestart === false) {
1911
- throw new CfDebuggerError(
1912
- "SSH_NOT_ENABLED",
1913
- `SSH is disabled for ${options.app}; automatic SSH enable and app restart are not allowed.`,
1914
- signalResult.stderr
1915
- );
1916
- }
1917
- await enableSshAndRestart(options, target, context, sessionId, emit);
1918
- return await retryRemoteSignal(options, target, context, sessionId, emit);
1919
- }
1920
- async function enableSshAndRestart(options, target, context, sessionId, emit) {
1921
- if (target.nodePid !== void 0) {
1922
- throw new CfDebuggerError(
1923
- "NODE_PID_RESTART_UNSAFE",
1924
- `Cannot automatically restart ${options.app} while targeting remote Node PID ${target.nodePid.toString()}. Enable SSH and restart the app first, then retry with its new PID.`
1925
- );
1926
- }
1927
- const alreadyEnabled = await cfSshEnabled(options.app, context);
1928
- if (!alreadyEnabled) {
1929
- emit("ssh-enabling", "Enabling SSH on the app");
1930
- await transitionStartupStatus(sessionId, "ssh-enabling");
1931
- await cfEnableSsh(options.app, context);
1932
- }
1933
- emit("ssh-restarting", "Restarting app so SSH becomes active");
1934
- await transitionStartupStatus(sessionId, "ssh-restarting");
1935
- await cfRestartApp(options.app, context);
1936
- checkAbort(context.signal);
1937
- }
1938
- async function retryRemoteSignal(options, target, context, sessionId, emit) {
1939
- emit("signaling");
1940
- await transitionStartupStatus(sessionId, "signaling");
1941
- const retrySignalResult = await executeRemoteSignal(options.app, target, context);
1942
- if (retrySignalResult.exitCode === 0) {
1943
- return parseSignalResult(options.app, retrySignalResult);
1944
- }
1945
- throw new CfDebuggerError(
1946
- "USR1_SIGNAL_FAILED",
1947
- `Failed to send SIGUSR1 to the Node.js process on ${options.app} after enabling SSH: ${signalFailureDetail(retrySignalResult)}`,
1948
- retrySignalResult.stderr
1949
- );
1950
- }
1951
- async function executeRemoteSignal(appName, target, context) {
1952
- return await cfSshOneShot(appName, buildNodeInspectorCommand(target.nodePid), context, {
1953
- process: target.process,
1954
- instance: target.instance
1955
- });
1956
- }
1957
- function parseSignalResult(appName, result) {
1958
- if (result.exitCode !== 0) {
1959
- throw new CfDebuggerError(
1960
- "USR1_SIGNAL_FAILED",
1961
- `Failed to send SIGUSR1 to the Node.js process on ${appName}: ${signalFailureDetail(result)}`,
1962
- result.stderr
1963
- );
1964
- }
1965
- if (result.outputTruncated) {
1966
- throw new CfDebuggerError(
1967
- "INSPECTOR_OUTPUT_TOO_LARGE",
1968
- "Inspector startup output exceeded the configured capture limit."
1969
- );
1970
- }
1971
- return parseNodeInspectorMarkers(result.stdout).remoteNodePid;
1972
- }
1973
- async function waitAfterSignal(signal) {
1974
- if (signal?.aborted) {
1975
- throw new CfDebuggerError("ABORTED", "Operation aborted by caller");
1976
- }
1977
- await new Promise((resolve, reject) => {
1978
- const onAbort = () => {
1979
- clearTimeout(timer);
1980
- reject(new CfDebuggerError("ABORTED", "Operation aborted by caller"));
1981
- };
1982
- const timer = setTimeout(() => {
1983
- signal?.removeEventListener("abort", onAbort);
1984
- resolve();
1985
- }, POST_USR1_DELAY_MS);
1986
- signal?.addEventListener("abort", onAbort, { once: true });
1987
- });
1988
- }
1989
- async function ensurePortAvailable(localPort) {
1990
- if (!await isPortFree(localPort)) {
1991
- throw new CfDebuggerError(
1992
- "PORT_UNAVAILABLE",
1993
- `Local port ${localPort.toString()} was taken before the tunnel could start.`
1994
- );
1995
- }
1996
- }
1997
- async function openReadyTunnel(options, target, session, context, tunnelReadyTimeoutMs, onChild) {
1998
- await ensurePortAvailable(session.localPort);
1999
- checkAbort(context.signal);
2000
- const child = spawnSshTunnel(options.app, session.localPort, session.remotePort, context, {
2001
- process: target.process,
2002
- instance: target.instance
2003
- });
2004
- onChild(child);
2005
- const childPid = child.pid;
2006
- if (childPid === void 0) {
2007
- throw new CfDebuggerError("TUNNEL_PROCESS_MISSING", "The CF SSH tunnel process did not expose a PID.");
2008
- }
2009
- const pidState = requireStartupState(await updateSessionPid(session.sessionId, childPid));
2010
- if (pidState.tunnelPid !== childPid || pidState.pid !== childPid) {
2011
- throw new CfDebuggerError(
2012
- "SESSION_STATE_CONFLICT",
2013
- "Debugger session did not retain ownership of the spawned tunnel process."
2014
- );
2015
- }
2016
- const ready = await probeTunnelReady(
2017
- session.localPort,
2018
- tunnelReadyTimeoutMs,
2019
- context.signal
2020
- );
2021
- checkAbort(context.signal);
2022
- if (!ready) {
2023
- throw new CfDebuggerError(
2024
- "TUNNEL_NOT_READY",
2025
- `SSH tunnel on port ${session.localPort.toString()} did not become ready within ${Math.round(tunnelReadyTimeoutMs / 1e3).toString()}s.`
2026
- );
2027
- }
2028
- const listeningPid = await findListeningProcessId(session.localPort);
2029
- if (listeningPid === void 0) {
2030
- throw new CfDebuggerError(
2031
- "TUNNEL_OWNER_UNVERIFIED",
2032
- `Could not verify the owner of local tunnel port ${session.localPort.toString()}.`
2033
- );
2034
- }
2035
- if (listeningPid !== childPid) {
2036
- throw new CfDebuggerError(
2037
- "TUNNEL_OWNER_MISMATCH",
2038
- `Local tunnel port ${session.localPort.toString()} is owned by an unexpected process.`
2039
- );
2040
- }
2041
- }
2042
- function attachTunnelEvents(child, resolveExit, emit) {
2043
- child.on("close", (code) => {
2044
- resolveExit(code);
2045
- });
2046
- child.on("error", (err) => {
2047
- emit("error", err.message);
2048
- });
2049
- }
2050
- function createHandle(session, emit, finalize, exitPromise) {
2051
- let disposePromise;
2052
- return {
2053
- session,
2054
- dispose: async () => {
2055
- const attempt = disposePromise ?? (async () => {
2056
- await runCleanupActions([
2057
- () => {
2058
- emit("stopping");
2059
- },
2060
- async () => {
2061
- await updateSessionStatus(session.sessionId, "stopping");
2062
- },
2063
- finalize
2064
- ], "Debugger disposal failed");
2065
- })();
2066
- disposePromise = attempt;
2067
- try {
2068
- await attempt;
2069
- } catch (error) {
2070
- if (disposePromise === attempt) {
2071
- disposePromise = void 0;
2072
- }
2073
- throw error;
2074
- }
2075
- },
2076
- waitForExit: async () => {
2077
- return await exitPromise;
2078
- }
2079
- };
2080
- }
2081
- async function runCleanupActions(actions, aggregateMessage) {
2082
- let errors = [];
2083
- for (const action of actions) {
2084
- try {
2085
- await action();
2086
- } catch (error) {
2087
- errors = [...errors, error];
2088
- }
2089
- }
2090
- if (errors.length === 1) {
2091
- throw errors[0];
2092
- }
2093
- if (errors.length > 1) {
2094
- throw new AggregateError(errors, aggregateMessage);
2095
- }
2096
- }
2097
- async function prepareCfHome(cfHomeDir) {
2098
- await mkdir3(cfHomeDir, { recursive: true, mode: 448 });
2099
- await chmod3(cfHomeDir, 448);
2100
- }
2101
- function createTunnelLifecycle(session, emit) {
2102
- let child;
2103
- let exitResolve = (_code) => {
2104
- throw new Error("Exit resolver was used before initialization");
2105
- };
2106
- const exitPromise = new Promise((resolve) => {
2107
- exitResolve = resolve;
2108
- });
2109
- const observeChild = (tunnelChild) => {
2110
- child = tunnelChild;
2111
- attachTunnelEvents(tunnelChild, exitResolve, emit);
2112
- };
2113
- const finalize = async () => {
2114
- const termination = child === void 0 ? "terminated" : await killProcessGroupOrProc(child);
2115
- const portListening = child !== void 0 && await isPortListening(session.localPort);
2116
- if (termination === "still-alive" || portListening) {
2117
- throw new CfDebuggerError(
2118
- "TUNNEL_TERMINATION_FAILED",
2119
- `Tunnel for session ${session.sessionId} did not terminate; state and CF home were retained.`
2120
- );
2121
- }
2122
- await runCleanupActions([
2123
- async () => {
2124
- await removeSession(session.sessionId);
2125
- },
2126
- async () => {
2127
- await cleanupFilesystem(session.cfHomeDir);
2128
- }
2129
- ], "Debugger resource cleanup failed");
2130
- emit("stopped");
2131
- };
2132
- return { exitPromise, finalize, observeChild };
2133
- }
2134
- async function establishDebuggerSession(inputs) {
2135
- const { options, target, session, context, credentials, timeoutMs, lifecycle, emit } = inputs;
2136
- await prepareCfHome(session.cfHomeDir);
2137
- await loginAndTarget(
2138
- options,
2139
- session.apiEndpoint,
2140
- credentials.email,
2141
- credentials.password,
2142
- context,
2143
- session.sessionId,
2144
- emit
2145
- );
2146
- await ensurePortAvailable(session.localPort);
2147
- const remoteNodePid = await signalRemoteNode(options, target, context, session.sessionId, emit);
2148
- const remoteState = requireStartupState(
2149
- await updateSessionRemoteNodePid(session.sessionId, remoteNodePid)
2150
- );
2151
- if (remoteState.remoteNodePid !== remoteNodePid) {
2152
- throw new CfDebuggerError(
2153
- "SESSION_STATE_CONFLICT",
2154
- "Debugger session did not retain the selected remote Node PID."
2155
- );
2156
- }
2157
- await waitAfterSignal(context.signal);
2158
- emit("tunneling");
2159
- await transitionStartupStatus(session.sessionId, "tunneling");
2160
- await openReadyTunnel(
2161
- options,
2162
- target,
2163
- session,
2164
- context,
2165
- timeoutMs,
2166
- lifecycle.observeChild
2167
- );
2168
- emit("ready");
2169
- return await transitionStartupStatus(session.sessionId, "ready");
2170
- }
2171
- async function failAfterStartupCleanup(error, finalize, emit) {
2172
- try {
2173
- await runCleanupActions([
2174
- () => {
2175
- emit("error", error instanceof Error ? error.message : String(error));
2176
- },
2177
- finalize
2178
- ], "Debugger startup failure reporting and cleanup failed");
2179
- } catch (cleanupError) {
2180
- throw new AggregateError(
2181
- [error, cleanupError],
2182
- "Debugger startup failed and resource cleanup was incomplete",
2183
- { cause: cleanupError }
2184
- );
2185
- }
2186
- throw error;
2187
- }
2188
- async function startDebugger(options) {
2189
- const target = resolveNodeTarget(options);
2190
- const credentials = requireCredentials(options);
2191
- const apiEndpoint = resolveApiEndpoint(options.region, options.apiEndpoint);
2192
- const tunnelReadyTimeoutMs = options.tunnelReadyTimeoutMs ?? DEFAULT_TUNNEL_READY_TIMEOUT_MS;
2193
- const emit = (status, message) => {
2194
- options.onStatus?.(status, message);
2195
- };
2196
- checkAbort(options.signal);
2197
- await pruneAndCleanupOrphans();
2198
- const session = await registerSession(options, target, apiEndpoint);
2199
- const cancellation = createStartupCancellation(session.sessionId, options.signal);
2200
- const context = {
2201
- cfHome: session.cfHomeDir,
2202
- signal: cancellation.signal
2203
- };
2204
- const lifecycle = createTunnelLifecycle(session, emit);
2205
- try {
2206
- const activeSession = await establishDebuggerSession({
2207
- options,
2208
- target,
2209
- session,
2210
- context,
2211
- credentials,
2212
- timeoutMs: tunnelReadyTimeoutMs,
2213
- lifecycle,
2214
- emit
2215
- });
2216
- cancellation.dispose();
2217
- return createHandle(activeSession, emit, lifecycle.finalize, lifecycle.exitPromise);
2218
- } catch (err) {
2219
- cancellation.dispose();
2220
- return await failAfterStartupCleanup(err, lifecycle.finalize, emit);
2221
- }
2222
- }
2223
- async function cleanupFilesystem(cfHomeDir) {
2224
- await rm2(cfHomeDir, { recursive: true, force: true });
2225
- }
2226
-
2227
- // src/debug-session/sessions.ts
2228
- import { rm as rm3 } from "fs/promises";
2229
- import { hostname as getHostname3 } from "os";
2230
- import process6 from "process";
2231
- function findMatchingSession(sessions, options) {
2232
- if (options.sessionId !== void 0) {
2233
- return sessions.find((s) => s.sessionId === options.sessionId);
2234
- }
2235
- if (options.key !== void 0) {
2236
- const key = options.key;
2237
- const matches = sessions.filter((session) => matchesKey(session, key));
2238
- if (matches.length > 1) {
2239
- throw new CfDebuggerError(
2240
- "SESSION_AMBIGUOUS",
2241
- "Multiple debugger sessions match this target. Pass an exact session ID, API endpoint, or Node PID."
2242
- );
2243
- }
2244
- return matches[0];
2245
- }
2246
- return void 0;
2247
- }
2248
- async function ownsRecordedTunnel(target) {
2249
- if (target.tunnelPid === void 0 || !await isPortListening(target.localPort)) {
2250
- return false;
2251
- }
2252
- return await findListeningProcessId(target.localPort) === target.tunnelPid;
2253
- }
2254
- async function terminateVerifiedTunnel(target) {
2255
- if (target.tunnelPid !== void 0 && target.tunnelPid !== process6.pid) {
2256
- try {
2257
- return await terminatePidOrGroup(target.tunnelPid);
2258
- } catch {
2259
- return "still-alive";
2260
- }
2261
- }
2262
- return target.tunnelPid === process6.pid ? "still-alive" : "terminated";
2263
- }
2264
- async function terminateVerifiedTunnelAndConfirm(target) {
2265
- const termination = await terminateVerifiedTunnel(target);
2266
- if (termination === "still-alive" || await ownsRecordedTunnel(target)) {
2267
- throw new CfDebuggerError(
2268
- "TUNNEL_TERMINATION_FAILED",
2269
- `Tunnel process for session ${target.sessionId} did not terminate; state was retained.`
2270
- );
2271
- }
2272
- }
2273
- async function cleanupOwnedCfHome(target, locallyOwned) {
2274
- if (locallyOwned && isOwnedSessionCfHomeDir(target.sessionId, target.cfHomeDir)) {
2275
- try {
2276
- await rm3(target.cfHomeDir, { recursive: true, force: true });
2277
- } catch {
2278
- }
2279
- }
2280
- }
2281
- async function removeOwnedSession(target, stale) {
2282
- const removed = await removeSession(target.sessionId);
2283
- await cleanupOwnedCfHome(target, true);
2284
- return { ...removed ?? target, stale, pending: false };
2285
- }
2286
- function ownershipError(target) {
2287
- return new CfDebuggerError(
2288
- "TUNNEL_OWNERSHIP_UNVERIFIED",
2289
- `Cannot safely stop session ${target.sessionId}: local tunnel ownership could not be verified.`
2290
- );
2291
- }
2292
- async function stopReadySession(target) {
2293
- if (await ownsRecordedTunnel(target)) {
2294
- await terminateVerifiedTunnelAndConfirm(target);
2295
- return await removeOwnedSession(target, false);
2296
- }
2297
- const tunnelDead = target.tunnelPid !== void 0 && !isPidOrGroupAlive(target.tunnelPid);
2298
- if (tunnelDead && !await isPortListening(target.localPort)) {
2299
- return await removeOwnedSession(target, true);
2300
- }
2301
- throw ownershipError(target);
2302
- }
2303
- async function stopStartingSession(target) {
2304
- if (target.status === "stopping" && target.tunnelPid !== void 0 && !isPidOrGroupAlive(target.tunnelPid) && !await isPortListening(target.localPort)) {
2305
- return await removeOwnedSession(target, true);
2306
- }
2307
- if (isPidAlive(target.controllerPid ?? target.pid)) {
2308
- return { ...target, stale: false, pending: true };
2309
- }
2310
- if (await ownsRecordedTunnel(target)) {
2311
- await terminateVerifiedTunnelAndConfirm(target);
2312
- return await removeOwnedSession(target, false);
2313
- }
2314
- const tunnelAlive = target.tunnelPid !== void 0 && isPidOrGroupAlive(target.tunnelPid);
2315
- if (!tunnelAlive && !await isPortListening(target.localPort)) {
2316
- return await removeOwnedSession(target, true);
2317
- }
2318
- throw ownershipError(target);
2319
- }
2320
- async function stopDebugger(options) {
2321
- const localSessions = (await readSessionSnapshot()).filter(
2322
- (session) => session.hostname === getHostname3()
2323
- );
2324
- const target = findMatchingSession(localSessions, options);
2325
- if (target === void 0) {
2326
- return void 0;
2327
- }
2328
- const claim = await requestSessionStop(target.sessionId);
2329
- if (claim === void 0) {
2330
- return void 0;
2331
- }
2332
- return claim.previousStatus === "ready" ? await stopReadySession(claim.session) : await stopStartingSession(claim.session);
2333
- }
2334
- async function stopAllDebuggers() {
2335
- const sessions = (await readSessionSnapshot()).filter(
2336
- (session) => session.hostname === getHostname3()
2337
- );
2338
- let stopped = 0;
2339
- for (const session of sessions) {
2340
- const result = await stopDebugger({ sessionId: session.sessionId });
2341
- if (result) {
2342
- stopped += 1;
2343
- }
2344
- }
2345
- return stopped;
2346
- }
2347
- async function listSessions() {
2348
- return await readActiveSessions();
2349
- }
2350
- async function getSession(key) {
2351
- const sessions = await readActiveSessions();
2352
- return findMatchingSession(sessions, { key });
2353
- }
1
+ import {
2
+ CfDebuggerError,
3
+ DEFAULT_NODE_INSPECTOR_PORT,
4
+ buildNodeInspectorCommand,
5
+ getSession,
6
+ listKnownRegionKeys,
7
+ listSessions,
8
+ parseCurrentCfTarget,
9
+ parseNodeInspectorMarkers,
10
+ readCurrentCfTarget,
11
+ regionKeyForApiEndpoint,
12
+ regionKeyFromSapApiEndpoint,
13
+ requireCurrentCfRegion,
14
+ resolveApiEndpoint,
15
+ resolveNodeTarget,
16
+ runDoctor,
17
+ sessionKeyString,
18
+ startDebugger,
19
+ stopAllDebuggers,
20
+ stopDebugger
21
+ } from "./chunk-ZWAQD7KL.js";
2354
22
  export {
2355
23
  CfDebuggerError,
24
+ DEFAULT_NODE_INSPECTOR_PORT,
2356
25
  buildNodeInspectorCommand,
2357
26
  getSession,
2358
27
  listKnownRegionKeys,
@@ -2360,9 +29,12 @@ export {
2360
29
  parseCurrentCfTarget,
2361
30
  parseNodeInspectorMarkers,
2362
31
  readCurrentCfTarget,
32
+ regionKeyForApiEndpoint,
33
+ regionKeyFromSapApiEndpoint,
2363
34
  requireCurrentCfRegion,
2364
35
  resolveApiEndpoint,
2365
36
  resolveNodeTarget,
37
+ runDoctor,
2366
38
  sessionKeyString,
2367
39
  startDebugger,
2368
40
  stopAllDebuggers,