@saptools/cf-debugger 0.2.0 → 0.2.2
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 +66 -29
- package/dist/{chunk-ZWAQD7KL.js → chunk-QBB4N2WN.js} +1671 -785
- package/dist/chunk-QBB4N2WN.js.map +1 -0
- package/dist/cli.js +101 -143
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +27 -9
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-ZWAQD7KL.js.map +0 -1
|
@@ -12,6 +12,32 @@ var CfDebuggerError = class extends Error {
|
|
|
12
12
|
}
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
// src/input-validation.ts
|
|
16
|
+
function hasControlCharacter(value) {
|
|
17
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
18
|
+
const code = value.charCodeAt(index);
|
|
19
|
+
if (code < 32 || code === 127) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
function validateCfCliOperand(value, label) {
|
|
26
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
27
|
+
throw new CfDebuggerError("UNSAFE_INPUT", `${label} must be a non-empty string.`);
|
|
28
|
+
}
|
|
29
|
+
if (value.trim() !== value) {
|
|
30
|
+
throw new CfDebuggerError("UNSAFE_INPUT", `${label} must not contain surrounding whitespace.`);
|
|
31
|
+
}
|
|
32
|
+
if (value.startsWith("-")) {
|
|
33
|
+
throw new CfDebuggerError("UNSAFE_INPUT", `${label} must not start with a hyphen.`);
|
|
34
|
+
}
|
|
35
|
+
if (hasControlCharacter(value)) {
|
|
36
|
+
throw new CfDebuggerError("UNSAFE_INPUT", `${label} must not contain control characters.`);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
15
41
|
// src/regions.ts
|
|
16
42
|
var REGION_KEY_PATTERN = /^[a-z]{2}\d{2}(?:-\d{3})?$/;
|
|
17
43
|
var REGION_API_ENDPOINTS = {
|
|
@@ -80,9 +106,61 @@ function synthesizeApiEndpoint(regionKey) {
|
|
|
80
106
|
const domain = regionKey.startsWith("cn") ? "platform.sapcloud.cn" : "hana.ondemand.com";
|
|
81
107
|
return `https://api.cf.${regionKey}.${domain}`;
|
|
82
108
|
}
|
|
109
|
+
function rejectApiEndpoint(raw, reason) {
|
|
110
|
+
return new CfDebuggerError(
|
|
111
|
+
"UNSAFE_INPUT",
|
|
112
|
+
`Invalid --api-endpoint ${JSON.stringify(raw)}: ${reason}. Expected an absolute https URL such as https://api.cf.<region>.hana.ondemand.com.`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
function validateApiEndpointOverride(raw) {
|
|
116
|
+
if (typeof raw !== "string") {
|
|
117
|
+
throw new CfDebuggerError(
|
|
118
|
+
"UNSAFE_INPUT",
|
|
119
|
+
"Invalid --api-endpoint value: it must be a string."
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const value = raw.trim();
|
|
123
|
+
if (value.length === 0) {
|
|
124
|
+
throw rejectApiEndpoint(raw, "the value is empty or whitespace only");
|
|
125
|
+
}
|
|
126
|
+
if (value !== raw) {
|
|
127
|
+
throw rejectApiEndpoint(raw, "it contains surrounding whitespace");
|
|
128
|
+
}
|
|
129
|
+
if (value.startsWith("-")) {
|
|
130
|
+
throw rejectApiEndpoint(raw, "a leading hyphen would be parsed as a cf CLI flag");
|
|
131
|
+
}
|
|
132
|
+
if (hasControlCharacter(value)) {
|
|
133
|
+
throw rejectApiEndpoint(raw, "it contains control characters");
|
|
134
|
+
}
|
|
135
|
+
let parsed;
|
|
136
|
+
try {
|
|
137
|
+
parsed = new URL(value);
|
|
138
|
+
} catch {
|
|
139
|
+
throw rejectApiEndpoint(raw, "it is not a parseable absolute URL");
|
|
140
|
+
}
|
|
141
|
+
if (parsed.protocol !== "https:") {
|
|
142
|
+
throw rejectApiEndpoint(
|
|
143
|
+
raw,
|
|
144
|
+
`the scheme must be https, not ${parsed.protocol.replace(":", "")} (credentials are sent to this endpoint)`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (parsed.username !== "" || parsed.password !== "") {
|
|
148
|
+
throw rejectApiEndpoint(raw, "it must not embed userinfo credentials");
|
|
149
|
+
}
|
|
150
|
+
if (parsed.search !== "" || parsed.hash !== "") {
|
|
151
|
+
throw rejectApiEndpoint(raw, "it must not carry a query string or fragment");
|
|
152
|
+
}
|
|
153
|
+
if (parsed.pathname !== "" && parsed.pathname !== "/") {
|
|
154
|
+
throw rejectApiEndpoint(raw, "it must not carry a path");
|
|
155
|
+
}
|
|
156
|
+
if (parsed.hostname === "") {
|
|
157
|
+
throw rejectApiEndpoint(raw, "it has no host");
|
|
158
|
+
}
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
83
161
|
function resolveApiEndpoint(regionKey, override, onWarning) {
|
|
84
|
-
if (override !== void 0
|
|
85
|
-
return override;
|
|
162
|
+
if (override !== void 0) {
|
|
163
|
+
return validateApiEndpointOverride(override);
|
|
86
164
|
}
|
|
87
165
|
if (!REGION_KEY_PATTERN.test(regionKey)) {
|
|
88
166
|
throw new CfDebuggerError(
|
|
@@ -110,6 +188,8 @@ import nodeProcess from "process";
|
|
|
110
188
|
var MAX_BUFFER = 16 * 1024 * 1024;
|
|
111
189
|
var DEFAULT_CF_COMMAND_TIMEOUT_MS = 6e4;
|
|
112
190
|
var DEFAULT_CF_OPERATION_TIMEOUT_MS = 3e5;
|
|
191
|
+
var MUTATING_CF_ATTEMPT_TIMEOUT_MS = 18e4;
|
|
192
|
+
var MUTATION_DIAGNOSTIC_RESERVE_MS = 250;
|
|
113
193
|
var REDACTED_ARG = "<redacted>";
|
|
114
194
|
var MAX_RETRY_DELAY_MS = 1e4;
|
|
115
195
|
function buildEnv(cfHome) {
|
|
@@ -159,12 +239,12 @@ function optionalString(value) {
|
|
|
159
239
|
}
|
|
160
240
|
function readFailureDetails(error) {
|
|
161
241
|
const object = typeof error === "object" && error !== null ? error : void 0;
|
|
162
|
-
const
|
|
242
|
+
const field4 = (key) => object === void 0 ? void 0 : Reflect.get(object, key);
|
|
163
243
|
return {
|
|
164
|
-
code: optionalString(
|
|
165
|
-
killed:
|
|
244
|
+
code: optionalString(field4("code")),
|
|
245
|
+
killed: field4("killed") === true,
|
|
166
246
|
message: error instanceof Error ? error.message : String(error),
|
|
167
|
-
stderr: optionalString(
|
|
247
|
+
stderr: optionalString(field4("stderr"))
|
|
168
248
|
};
|
|
169
249
|
}
|
|
170
250
|
function hasTransientDiagnostic(error) {
|
|
@@ -268,57 +348,72 @@ function signalCfChild(child, signal) {
|
|
|
268
348
|
} catch {
|
|
269
349
|
}
|
|
270
350
|
}
|
|
351
|
+
function rejectBoundedExecution(settlement, cleanup, reject, failure) {
|
|
352
|
+
if (settlement.settled) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
settlement.settled = true;
|
|
356
|
+
cleanup();
|
|
357
|
+
reject(failure);
|
|
358
|
+
}
|
|
359
|
+
function handleBoundedCompletion(settlement, cleanup, resolve, reject, signal, error, stdout, stderr) {
|
|
360
|
+
if (settlement.settled) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (error === null && signal?.aborted !== true) {
|
|
364
|
+
settlement.settled = true;
|
|
365
|
+
cleanup();
|
|
366
|
+
resolve({ stderr, stdout });
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const failure = error ?? new Error("Command was aborted.");
|
|
370
|
+
Reflect.set(failure, "stderr", stderr);
|
|
371
|
+
rejectBoundedExecution(settlement, cleanup, reject, failure);
|
|
372
|
+
}
|
|
373
|
+
function terminateBoundedExecution(settlement, cleanup, reject, child, code, message) {
|
|
374
|
+
if (settlement.settled) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
signalCfChild(child, "SIGKILL");
|
|
378
|
+
child.stdout?.destroy();
|
|
379
|
+
child.stderr?.destroy();
|
|
380
|
+
const failure = new Error(message);
|
|
381
|
+
Reflect.set(failure, "code", code);
|
|
382
|
+
Reflect.set(failure, "killed", true);
|
|
383
|
+
Reflect.set(failure, "stderr", "");
|
|
384
|
+
rejectBoundedExecution(settlement, cleanup, reject, failure);
|
|
385
|
+
}
|
|
271
386
|
async function executeFileBounded(command, args, options) {
|
|
272
387
|
return await new Promise((resolve, reject) => {
|
|
273
|
-
|
|
388
|
+
const settlement = { settled: false };
|
|
274
389
|
const cleanup = () => {
|
|
275
390
|
clearTimeout(timer);
|
|
276
391
|
options.signal?.removeEventListener("abort", onAbort);
|
|
277
392
|
};
|
|
278
|
-
const rejectOnce = (error) => {
|
|
279
|
-
if (settled) {
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
settled = true;
|
|
283
|
-
cleanup();
|
|
284
|
-
reject(error);
|
|
285
|
-
};
|
|
286
393
|
const child = execFile(command, [...args], {
|
|
287
394
|
env: options.env,
|
|
288
395
|
maxBuffer: options.maxBuffer ?? MAX_BUFFER,
|
|
289
396
|
encoding: "utf8"
|
|
290
397
|
}, (error, stdout, stderr) => {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
Reflect.set(failure, "stderr", stderr);
|
|
302
|
-
reject(failure);
|
|
398
|
+
handleBoundedCompletion(
|
|
399
|
+
settlement,
|
|
400
|
+
cleanup,
|
|
401
|
+
resolve,
|
|
402
|
+
reject,
|
|
403
|
+
options.signal,
|
|
404
|
+
error,
|
|
405
|
+
stdout,
|
|
406
|
+
stderr
|
|
407
|
+
);
|
|
303
408
|
});
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
signalCfChild(child, "SIGKILL");
|
|
309
|
-
child.stdout?.destroy();
|
|
310
|
-
child.stderr?.destroy();
|
|
311
|
-
const failure = new Error(message);
|
|
312
|
-
Reflect.set(failure, "code", code);
|
|
313
|
-
Reflect.set(failure, "killed", true);
|
|
314
|
-
Reflect.set(failure, "stderr", "");
|
|
315
|
-
rejectOnce(failure);
|
|
409
|
+
const terminate = (code, message) => {
|
|
410
|
+
terminateBoundedExecution(settlement, cleanup, reject, child, code, message);
|
|
316
411
|
};
|
|
317
412
|
const onAbort = () => {
|
|
318
|
-
|
|
413
|
+
terminate("ABORT_ERR", "Command was aborted.");
|
|
319
414
|
};
|
|
320
415
|
const timer = setTimeout(() => {
|
|
321
|
-
|
|
416
|
+
terminate("ETIMEDOUT", "Command exceeded its execution deadline.");
|
|
322
417
|
}, options.timeoutMs);
|
|
323
418
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
324
419
|
if (options.signal?.aborted) {
|
|
@@ -339,6 +434,13 @@ function createCfCliError(args, failure, redactionValues) {
|
|
|
339
434
|
stderr
|
|
340
435
|
);
|
|
341
436
|
}
|
|
437
|
+
function mutationTimeoutError(args, redactionValues) {
|
|
438
|
+
const verb = args[0] ?? "command";
|
|
439
|
+
return new CfDebuggerError(
|
|
440
|
+
"CF_MUTATION_TIMEOUT",
|
|
441
|
+
`cf ${formatArgsForError(args, redactionValues)} did not complete within its deadline and was not retried, because it mutates the deployed application. The ${verb} may still be completing on the platform; check the application state before retrying.`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
342
444
|
function timeoutError(context, args, redactionValues) {
|
|
343
445
|
if (context.deadlineAt !== void 0) {
|
|
344
446
|
const timeoutMs = context.startupTimeoutMs;
|
|
@@ -361,46 +463,69 @@ function reportRetry(context, args, attempt, delayMs, remainingMs, redactionValu
|
|
|
361
463
|
remainingMs
|
|
362
464
|
});
|
|
363
465
|
}
|
|
364
|
-
|
|
466
|
+
function planCfAttempt(command, options, context) {
|
|
467
|
+
const remainingMs = options.deadlineAt - Date.now();
|
|
468
|
+
if (remainingMs <= 0) {
|
|
469
|
+
throw timeoutError(context, command.args, options.redactionValues);
|
|
470
|
+
}
|
|
471
|
+
const attemptCap = command.retryPolicy === "mutation" ? Math.min(MUTATING_CF_ATTEMPT_TIMEOUT_MS, options.attemptTimeoutMs * 3) : options.attemptTimeoutMs;
|
|
472
|
+
const attemptBudget = command.retryPolicy === "mutation" ? Math.max(1, remainingMs - MUTATION_DIAGNOSTIC_RESERVE_MS) : remainingMs;
|
|
473
|
+
return {
|
|
474
|
+
// A mutating child must time out before the overall startup signal so the
|
|
475
|
+
// user receives the specific "mutation may still be in flight" diagnostic.
|
|
476
|
+
timeoutMs: Math.max(1, Math.min(attemptCap, attemptBudget))
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
async function handleCfAttemptFailure(error, command, context, options, attempt, attemptTimeoutMs, startedAt) {
|
|
480
|
+
const failure = readFailureDetails(error);
|
|
481
|
+
if (context.signal?.aborted || failure.code === "ABORT_ERR") {
|
|
482
|
+
throw abortError(context);
|
|
483
|
+
}
|
|
484
|
+
const elapsedMs = Date.now() - startedAt;
|
|
485
|
+
const attemptTimedOut = failure.killed && elapsedMs + 100 >= attemptTimeoutMs;
|
|
486
|
+
if (command.retryPolicy === "mutation") {
|
|
487
|
+
throw attemptTimedOut ? mutationTimeoutError(command.args, options.redactionValues) : createCfCliError(command.args, failure, options.redactionValues);
|
|
488
|
+
}
|
|
489
|
+
if (isCredentialRejection(command.args, failure) || !isTransientNetworkError(failure, attemptTimedOut)) {
|
|
490
|
+
throw createCfCliError(command.args, failure, options.redactionValues);
|
|
491
|
+
}
|
|
492
|
+
const delayMs = retryDelayMs(attempt);
|
|
493
|
+
const remainingMs = options.deadlineAt - Date.now();
|
|
494
|
+
if (remainingMs <= delayMs) {
|
|
495
|
+
throw timeoutError(context, command.args, options.redactionValues);
|
|
496
|
+
}
|
|
497
|
+
reportRetry(
|
|
498
|
+
context,
|
|
499
|
+
command.args,
|
|
500
|
+
attempt,
|
|
501
|
+
delayMs,
|
|
502
|
+
remainingMs,
|
|
503
|
+
options.redactionValues
|
|
504
|
+
);
|
|
505
|
+
await waitForRetry(delayMs, context);
|
|
506
|
+
}
|
|
507
|
+
async function runCf(command, context, input = {}) {
|
|
365
508
|
if (context.signal?.aborted) {
|
|
366
509
|
throw abortError(context);
|
|
367
510
|
}
|
|
368
511
|
const options = resolveRunOptions(context, input);
|
|
369
512
|
let attempt = 0;
|
|
370
513
|
for (; ; ) {
|
|
371
|
-
const
|
|
372
|
-
if (remainingMs <= 0) {
|
|
373
|
-
throw timeoutError(context, args, options.redactionValues);
|
|
374
|
-
}
|
|
514
|
+
const plan = planCfAttempt(command, options, context);
|
|
375
515
|
attempt += 1;
|
|
376
|
-
const attemptTimeoutMs = Math.max(1, Math.min(options.attemptTimeoutMs, remainingMs));
|
|
377
516
|
const startedAt = Date.now();
|
|
378
517
|
try {
|
|
379
|
-
return await executeCfAttempt(args, context, options,
|
|
518
|
+
return await executeCfAttempt(command.args, context, options, plan.timeoutMs);
|
|
380
519
|
} catch (error) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
const elapsedMs = Date.now() - startedAt;
|
|
386
|
-
const attemptTimedOut = failure.killed && elapsedMs + 100 >= attemptTimeoutMs;
|
|
387
|
-
if (isCredentialRejection(args, failure) || !isTransientNetworkError(failure, attemptTimedOut)) {
|
|
388
|
-
throw createCfCliError(args, failure, options.redactionValues);
|
|
389
|
-
}
|
|
390
|
-
const delayMs = retryDelayMs(attempt);
|
|
391
|
-
const afterFailureMs = options.deadlineAt - Date.now();
|
|
392
|
-
if (afterFailureMs <= delayMs) {
|
|
393
|
-
throw timeoutError(context, args, options.redactionValues);
|
|
394
|
-
}
|
|
395
|
-
reportRetry(
|
|
520
|
+
await handleCfAttemptFailure(
|
|
521
|
+
error,
|
|
522
|
+
command,
|
|
396
523
|
context,
|
|
397
|
-
|
|
524
|
+
options,
|
|
398
525
|
attempt,
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
options.redactionValues
|
|
526
|
+
plan.timeoutMs,
|
|
527
|
+
startedAt
|
|
402
528
|
);
|
|
403
|
-
await waitForRetry(delayMs, context);
|
|
404
529
|
}
|
|
405
530
|
}
|
|
406
531
|
}
|
|
@@ -408,16 +533,22 @@ async function runCf(args, context, input = {}) {
|
|
|
408
533
|
// src/cloud-foundry/commands.ts
|
|
409
534
|
var CURRENT_TARGET_TIMEOUT_MS = 3e4;
|
|
410
535
|
function rethrowControlFlowError(error) {
|
|
411
|
-
if (error instanceof CfDebuggerError && (error.code === "ABORTED" || error.code === "STARTUP_TIMEOUT")) {
|
|
536
|
+
if (error instanceof CfDebuggerError && (error.code === "ABORTED" || error.code === "CF_MUTATION_TIMEOUT" || error.code === "STARTUP_TIMEOUT")) {
|
|
412
537
|
throw error;
|
|
413
538
|
}
|
|
414
539
|
}
|
|
415
540
|
async function cfApi(apiEndpoint, context) {
|
|
416
|
-
await runCf(
|
|
541
|
+
await runCf({
|
|
542
|
+
args: ["api", apiEndpoint],
|
|
543
|
+
retryPolicy: "retry-transient"
|
|
544
|
+
}, context);
|
|
417
545
|
}
|
|
418
546
|
async function cfAuth(email, password, context) {
|
|
419
547
|
try {
|
|
420
|
-
await runCf(
|
|
548
|
+
await runCf({
|
|
549
|
+
args: ["auth"],
|
|
550
|
+
retryPolicy: "retry-transient"
|
|
551
|
+
}, context, {
|
|
421
552
|
env: { CF_PASSWORD: password, CF_USERNAME: email },
|
|
422
553
|
sensitiveValues: [email, password]
|
|
423
554
|
});
|
|
@@ -447,7 +578,10 @@ async function cfLogin(apiEndpoint, email, password, context) {
|
|
|
447
578
|
}
|
|
448
579
|
async function cfTarget(org, space, context) {
|
|
449
580
|
try {
|
|
450
|
-
await runCf(
|
|
581
|
+
await runCf({
|
|
582
|
+
args: ["target", "-o", org, "-s", space],
|
|
583
|
+
retryPolicy: "retry-transient"
|
|
584
|
+
}, context);
|
|
451
585
|
} catch (err) {
|
|
452
586
|
rethrowControlFlowError(err);
|
|
453
587
|
if (err instanceof CfDebuggerError) {
|
|
@@ -458,7 +592,10 @@ async function cfTarget(org, space, context) {
|
|
|
458
592
|
}
|
|
459
593
|
async function cfAppExists(appName, context) {
|
|
460
594
|
try {
|
|
461
|
-
await runCf(
|
|
595
|
+
await runCf({
|
|
596
|
+
args: ["app", appName],
|
|
597
|
+
retryPolicy: "retry-transient"
|
|
598
|
+
}, context);
|
|
462
599
|
return true;
|
|
463
600
|
} catch (err) {
|
|
464
601
|
rethrowControlFlowError(err);
|
|
@@ -471,7 +608,10 @@ async function cfAppExists(appName, context) {
|
|
|
471
608
|
}
|
|
472
609
|
async function cfSshEnabled(appName, context) {
|
|
473
610
|
try {
|
|
474
|
-
const stdout = await runCf(
|
|
611
|
+
const stdout = await runCf({
|
|
612
|
+
args: ["ssh-enabled", appName],
|
|
613
|
+
retryPolicy: "retry-transient"
|
|
614
|
+
}, context);
|
|
475
615
|
const normalized = stdout.toLowerCase();
|
|
476
616
|
if (normalized.includes("ssh support is enabled")) {
|
|
477
617
|
return "enabled";
|
|
@@ -487,7 +627,10 @@ async function cfSshEnabled(appName, context) {
|
|
|
487
627
|
}
|
|
488
628
|
async function cfEnableSsh(appName, context) {
|
|
489
629
|
try {
|
|
490
|
-
await runCf(
|
|
630
|
+
await runCf({
|
|
631
|
+
args: ["enable-ssh", appName],
|
|
632
|
+
retryPolicy: "mutation"
|
|
633
|
+
}, context);
|
|
491
634
|
} catch (err) {
|
|
492
635
|
rethrowControlFlowError(err);
|
|
493
636
|
if (err instanceof CfDebuggerError) {
|
|
@@ -497,7 +640,10 @@ async function cfEnableSsh(appName, context) {
|
|
|
497
640
|
}
|
|
498
641
|
}
|
|
499
642
|
async function cfRestartApp(appName, context) {
|
|
500
|
-
await runCf(
|
|
643
|
+
await runCf({
|
|
644
|
+
args: ["restart", appName],
|
|
645
|
+
retryPolicy: "mutation"
|
|
646
|
+
}, context);
|
|
501
647
|
}
|
|
502
648
|
async function readCurrentCfTarget(options = {}) {
|
|
503
649
|
try {
|
|
@@ -556,7 +702,7 @@ function parseTargetFields(stdout) {
|
|
|
556
702
|
line.slice(0, separator).trim().toLowerCase(),
|
|
557
703
|
line.slice(separator + 1).trim()
|
|
558
704
|
];
|
|
559
|
-
}).filter((
|
|
705
|
+
}).filter((field4) => field4 !== void 0)
|
|
560
706
|
);
|
|
561
707
|
}
|
|
562
708
|
function regionKeyForApiEndpoint(apiEndpoint) {
|
|
@@ -622,15 +768,6 @@ function resolveNodeTarget(input) {
|
|
|
622
768
|
validateNodePid(input.nodePid);
|
|
623
769
|
return input.nodePid === void 0 ? { process: processName, instance } : { process: processName, instance, nodePid: input.nodePid };
|
|
624
770
|
}
|
|
625
|
-
function hasControlCharacter(value) {
|
|
626
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
627
|
-
const code = value.charCodeAt(index);
|
|
628
|
-
if (code < 32 || code === 127) {
|
|
629
|
-
return true;
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
return false;
|
|
633
|
-
}
|
|
634
771
|
function buildNodeInspectorCommand(nodePid, remotePort = DEFAULT_NODE_INSPECTOR_PORT) {
|
|
635
772
|
validateNodePid(nodePid);
|
|
636
773
|
validateRemotePort(remotePort);
|
|
@@ -845,14 +982,15 @@ var NODE_INSPECTOR_SCRIPT = [
|
|
|
845
982
|
].join("\n");
|
|
846
983
|
|
|
847
984
|
// src/cloud-foundry/ssh.ts
|
|
848
|
-
import { spawn } from "child_process";
|
|
849
|
-
import
|
|
985
|
+
import { spawn as spawn2 } from "child_process";
|
|
986
|
+
import nodeProcess4 from "process";
|
|
850
987
|
import { StringDecoder } from "string_decoder";
|
|
988
|
+
|
|
989
|
+
// src/cloud-foundry/ssh-shared.ts
|
|
851
990
|
var DEFAULT_MAX_OUTPUT_BYTES = 65536;
|
|
852
991
|
var LIVE_LINE_LIMIT_BYTES = 65536;
|
|
853
992
|
var MAX_REDACTION_OVERLAP_BYTES = 4096;
|
|
854
993
|
var SENSITIVE_OUTPUT_OMITTED = "[diagnostic output omitted to protect a sensitive value]";
|
|
855
|
-
var tunnelCaptures = /* @__PURE__ */ new WeakMap();
|
|
856
994
|
function buildCfSshArgs(appName, target, tail) {
|
|
857
995
|
const resolved = resolveNodeTarget(target);
|
|
858
996
|
const processArgs = resolved.process === DEFAULT_CF_PROCESS ? [] : ["--process", resolved.process];
|
|
@@ -865,35 +1003,6 @@ function buildCfSshArgs(appName, target, tail) {
|
|
|
865
1003
|
...tail
|
|
866
1004
|
];
|
|
867
1005
|
}
|
|
868
|
-
async function cfSshOneShot(appName, command, context, rawOptions = DEFAULT_CF_COMMAND_TIMEOUT_MS) {
|
|
869
|
-
if (context.signal?.aborted || isDeadlineExpired(context)) {
|
|
870
|
-
throw sshAbortError(context);
|
|
871
|
-
}
|
|
872
|
-
const options = resolveSshOptions(rawOptions, context);
|
|
873
|
-
const args = buildCfSshArgs(appName, options.target, [
|
|
874
|
-
"--disable-pseudo-tty",
|
|
875
|
-
"-c",
|
|
876
|
-
command
|
|
877
|
-
]);
|
|
878
|
-
return await runSshOneShot(args, context, options);
|
|
879
|
-
}
|
|
880
|
-
function resolveSshOptions(raw, context) {
|
|
881
|
-
const input = typeof raw === "number" ? { timeoutMs: raw } : raw;
|
|
882
|
-
const requestedTimeoutMs = input.timeoutMs ?? DEFAULT_CF_COMMAND_TIMEOUT_MS;
|
|
883
|
-
const maxOutputBytes = input.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
884
|
-
if (!Number.isSafeInteger(requestedTimeoutMs) || requestedTimeoutMs <= 0) {
|
|
885
|
-
throw new CfDebuggerError("UNSAFE_INPUT", "timeoutMs must be a positive safe integer.");
|
|
886
|
-
}
|
|
887
|
-
if (!Number.isSafeInteger(maxOutputBytes) || maxOutputBytes <= 0) {
|
|
888
|
-
throw new CfDebuggerError("UNSAFE_INPUT", "maxOutputBytes must be a positive safe integer.");
|
|
889
|
-
}
|
|
890
|
-
const remainingMs = context.deadlineAt === void 0 ? requestedTimeoutMs : Math.max(1, context.deadlineAt - Date.now());
|
|
891
|
-
return {
|
|
892
|
-
target: input,
|
|
893
|
-
timeoutMs: Math.min(requestedTimeoutMs, remainingMs),
|
|
894
|
-
maxOutputBytes
|
|
895
|
-
};
|
|
896
|
-
}
|
|
897
1006
|
function createBoundedOutput() {
|
|
898
1007
|
return { chunks: [], bytes: 0, truncated: false };
|
|
899
1008
|
}
|
|
@@ -934,9 +1043,6 @@ function appendTail(output, data, outputLimit, retainedLimit) {
|
|
|
934
1043
|
}
|
|
935
1044
|
}
|
|
936
1045
|
}
|
|
937
|
-
function outputText(output) {
|
|
938
|
-
return Buffer.concat(output.chunks, output.bytes).toString("utf8");
|
|
939
|
-
}
|
|
940
1046
|
function createRedactionPolicy(values) {
|
|
941
1047
|
const sensitiveValues = normalizeSensitiveValues(values);
|
|
942
1048
|
const overlapBytes = sensitiveValues.reduce(
|
|
@@ -949,6 +1055,9 @@ function createRedactionPolicy(values) {
|
|
|
949
1055
|
overlapBytes: Math.min(overlapBytes, MAX_REDACTION_OVERLAP_BYTES)
|
|
950
1056
|
};
|
|
951
1057
|
}
|
|
1058
|
+
function outputText(output) {
|
|
1059
|
+
return Buffer.concat(output.chunks, output.bytes).toString("utf8");
|
|
1060
|
+
}
|
|
952
1061
|
function limitText(text, limit, keep) {
|
|
953
1062
|
const buffer = Buffer.from(text);
|
|
954
1063
|
if (buffer.byteLength <= limit) {
|
|
@@ -964,37 +1073,6 @@ function safeOutputText(output, policy, limit, keep) {
|
|
|
964
1073
|
const redacted = redactSensitiveText(outputText(output), policy.sensitiveValues);
|
|
965
1074
|
return limitText(redacted, limit, keep);
|
|
966
1075
|
}
|
|
967
|
-
function createResult(exitCode, state) {
|
|
968
|
-
const stdout = safeOutputText(state.stdout, state.redaction, state.outputLimit, "head");
|
|
969
|
-
const stderr = safeOutputText(state.stderr, state.redaction, state.outputLimit, "head");
|
|
970
|
-
return {
|
|
971
|
-
exitCode,
|
|
972
|
-
stdout,
|
|
973
|
-
stderr,
|
|
974
|
-
outputTruncated: state.stdout.truncated || state.stderr.truncated,
|
|
975
|
-
stdoutTruncated: state.stdout.truncated,
|
|
976
|
-
stderrTruncated: state.stderr.truncated
|
|
977
|
-
};
|
|
978
|
-
}
|
|
979
|
-
function createSshExecutionState(context, outputLimit) {
|
|
980
|
-
const redaction = createRedactionPolicy(context.sensitiveValues ?? []);
|
|
981
|
-
return {
|
|
982
|
-
stdout: createBoundedOutput(),
|
|
983
|
-
stderr: createBoundedOutput(),
|
|
984
|
-
outputLimit,
|
|
985
|
-
retainedLimit: outputLimit + redaction.overlapBytes,
|
|
986
|
-
redaction,
|
|
987
|
-
settled: false,
|
|
988
|
-
aborted: false,
|
|
989
|
-
timedOut: false
|
|
990
|
-
};
|
|
991
|
-
}
|
|
992
|
-
function terminateSshExecution(child, state) {
|
|
993
|
-
signalChild(child, "SIGTERM");
|
|
994
|
-
state.forceKillTimer ??= setTimeout(() => {
|
|
995
|
-
signalChild(child, "SIGKILL");
|
|
996
|
-
}, 1e3);
|
|
997
|
-
}
|
|
998
1076
|
function sshAbortError(context) {
|
|
999
1077
|
if (context.deadlineAt !== void 0 && Date.now() >= context.deadlineAt) {
|
|
1000
1078
|
return new CfDebuggerError(
|
|
@@ -1007,79 +1085,312 @@ function sshAbortError(context) {
|
|
|
1007
1085
|
function isDeadlineExpired(context) {
|
|
1008
1086
|
return context.deadlineAt !== void 0 && Date.now() >= context.deadlineAt;
|
|
1009
1087
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
const onAbort = () => {
|
|
1029
|
-
state.aborted = true;
|
|
1030
|
-
terminateSshExecution(child, state);
|
|
1031
|
-
};
|
|
1032
|
-
const settle = createSshSettler(state, options, context, onAbort, resolve, reject);
|
|
1033
|
-
state.timeoutTimer = setTimeout(() => {
|
|
1034
|
-
state.timedOut = true;
|
|
1035
|
-
terminateSshExecution(child, state);
|
|
1036
|
-
}, options.timeoutMs);
|
|
1037
|
-
if (context.signal?.aborted) {
|
|
1038
|
-
onAbort();
|
|
1039
|
-
} else {
|
|
1040
|
-
context.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1088
|
+
|
|
1089
|
+
// src/cloud-foundry/ssh-one-shot.ts
|
|
1090
|
+
import {
|
|
1091
|
+
spawn
|
|
1092
|
+
} from "child_process";
|
|
1093
|
+
import nodeProcess3 from "process";
|
|
1094
|
+
|
|
1095
|
+
// src/debug-session/process-identity.ts
|
|
1096
|
+
import { execFile as execFile2 } from "child_process";
|
|
1097
|
+
import { readFile } from "fs/promises";
|
|
1098
|
+
import nodeProcess2 from "process";
|
|
1099
|
+
import { promisify } from "util";
|
|
1100
|
+
var execFileAsync = promisify(execFile2);
|
|
1101
|
+
var PROCESS_IDENTITY_VERSION = "v1";
|
|
1102
|
+
var DARWIN_START_TIME_PATTERN = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) +(?:[1-9]|[12]\d|3[01]) \d{2}:\d{2}:\d{2} \d{4}$/;
|
|
1103
|
+
function errorCode(error) {
|
|
1104
|
+
if (typeof error !== "object" || error === null) {
|
|
1105
|
+
return void 0;
|
|
1041
1106
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
});
|
|
1045
|
-
child.stderr.on("data", (data) => {
|
|
1046
|
-
appendHead(state.stderr, data, state.outputLimit, state.retainedLimit);
|
|
1047
|
-
});
|
|
1048
|
-
child.on("close", (code, signal) => {
|
|
1049
|
-
const base = createResult(code, state);
|
|
1050
|
-
settle(state.timedOut || signal === null ? base : { ...base, signal });
|
|
1051
|
-
});
|
|
1052
|
-
child.on("error", (error) => {
|
|
1053
|
-
appendHead(state.stderr, error.message, state.outputLimit, state.retainedLimit);
|
|
1054
|
-
settle(createResult(null, state));
|
|
1055
|
-
});
|
|
1107
|
+
const code = Reflect.get(error, "code");
|
|
1108
|
+
return typeof code === "string" ? code : void 0;
|
|
1056
1109
|
}
|
|
1057
|
-
function
|
|
1058
|
-
|
|
1059
|
-
|
|
1110
|
+
function parseLinuxProcessStartTime(statLine) {
|
|
1111
|
+
const commandEnd = statLine.lastIndexOf(")");
|
|
1112
|
+
if (commandEnd < 0) {
|
|
1113
|
+
return void 0;
|
|
1060
1114
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
detached: nodeProcess2.platform !== "win32",
|
|
1065
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1066
|
-
});
|
|
1067
|
-
attachSshExecution(child, context, options, resolve, reject);
|
|
1068
|
-
});
|
|
1115
|
+
const fieldsAfterCommand = statLine.slice(commandEnd + 1).trim().split(/\s+/);
|
|
1116
|
+
const startTime = fieldsAfterCommand[19];
|
|
1117
|
+
return startTime !== void 0 && /^\d+$/.test(startTime) ? startTime : void 0;
|
|
1069
1118
|
}
|
|
1070
|
-
function
|
|
1071
|
-
if (
|
|
1072
|
-
|
|
1073
|
-
nodeProcess2.kill(-child.pid, signal);
|
|
1074
|
-
return;
|
|
1075
|
-
} catch {
|
|
1076
|
-
}
|
|
1119
|
+
function throwIfAborted(signal) {
|
|
1120
|
+
if (signal?.aborted) {
|
|
1121
|
+
throw new CfDebuggerError("ABORTED", "Process identity inspection was aborted.");
|
|
1077
1122
|
}
|
|
1123
|
+
}
|
|
1124
|
+
async function linuxProcessIdentity(pid, signal) {
|
|
1078
1125
|
try {
|
|
1079
|
-
|
|
1080
|
-
|
|
1126
|
+
const statLine = await readFile(`/proc/${pid.toString()}/stat`, {
|
|
1127
|
+
encoding: "utf8",
|
|
1128
|
+
...signal === void 0 ? {} : { signal }
|
|
1129
|
+
});
|
|
1130
|
+
const startTime = parseLinuxProcessStartTime(statLine);
|
|
1131
|
+
return startTime === void 0 ? void 0 : `linux:${PROCESS_IDENTITY_VERSION}:${startTime}`;
|
|
1132
|
+
} catch (error) {
|
|
1133
|
+
throwIfAborted(signal);
|
|
1134
|
+
if (errorCode(error) === "ENOENT") {
|
|
1135
|
+
return void 0;
|
|
1136
|
+
}
|
|
1137
|
+
return void 0;
|
|
1081
1138
|
}
|
|
1082
1139
|
}
|
|
1140
|
+
function darwinIdentityEnvironment(environment) {
|
|
1141
|
+
return {
|
|
1142
|
+
...environment,
|
|
1143
|
+
LC_ALL: "C",
|
|
1144
|
+
TZ: "UTC"
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
async function darwinProcessIdentity(pid, signal) {
|
|
1148
|
+
try {
|
|
1149
|
+
const { stdout } = await execFileAsync("ps", ["-p", pid.toString(), "-o", "lstart="], {
|
|
1150
|
+
env: darwinIdentityEnvironment(nodeProcess2.env),
|
|
1151
|
+
...signal === void 0 ? {} : { signal },
|
|
1152
|
+
timeout: 2e3
|
|
1153
|
+
});
|
|
1154
|
+
const startedAt = stdout.trim();
|
|
1155
|
+
return startedAt.length === 0 ? void 0 : `darwin:${PROCESS_IDENTITY_VERSION}:${startedAt}`;
|
|
1156
|
+
} catch {
|
|
1157
|
+
throwIfAborted(signal);
|
|
1158
|
+
return void 0;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
async function readProcessIdentity(pid, signal) {
|
|
1162
|
+
throwIfAborted(signal);
|
|
1163
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
1164
|
+
return void 0;
|
|
1165
|
+
}
|
|
1166
|
+
if (nodeProcess2.platform === "linux") {
|
|
1167
|
+
return await linuxProcessIdentity(pid, signal);
|
|
1168
|
+
}
|
|
1169
|
+
if (nodeProcess2.platform === "darwin") {
|
|
1170
|
+
return await darwinProcessIdentity(pid, signal);
|
|
1171
|
+
}
|
|
1172
|
+
return void 0;
|
|
1173
|
+
}
|
|
1174
|
+
function isCurrentProcessIdentity(identity) {
|
|
1175
|
+
if (/^linux:v1:\d+$/.test(identity)) {
|
|
1176
|
+
return true;
|
|
1177
|
+
}
|
|
1178
|
+
const darwinPrefix = `darwin:${PROCESS_IDENTITY_VERSION}:`;
|
|
1179
|
+
return identity.startsWith(darwinPrefix) && DARWIN_START_TIME_PATTERN.test(identity.slice(darwinPrefix.length));
|
|
1180
|
+
}
|
|
1181
|
+
async function inspectProcessIdentity(pid, expected, signal) {
|
|
1182
|
+
throwIfAborted(signal);
|
|
1183
|
+
if (expected === void 0) {
|
|
1184
|
+
return "match";
|
|
1185
|
+
}
|
|
1186
|
+
if (!isCurrentProcessIdentity(expected)) {
|
|
1187
|
+
return "unavailable";
|
|
1188
|
+
}
|
|
1189
|
+
const current = await readProcessIdentity(pid, signal);
|
|
1190
|
+
if (current === void 0 || !isCurrentProcessIdentity(current)) {
|
|
1191
|
+
return "unavailable";
|
|
1192
|
+
}
|
|
1193
|
+
return current === expected ? "match" : "mismatch";
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// src/cloud-foundry/ssh-one-shot.ts
|
|
1197
|
+
async function cfSshOneShot(appName, command, context, rawOptions = DEFAULT_CF_COMMAND_TIMEOUT_MS) {
|
|
1198
|
+
if (context.signal?.aborted || isDeadlineExpired(context)) {
|
|
1199
|
+
throw sshAbortError(context);
|
|
1200
|
+
}
|
|
1201
|
+
const options = resolveSshOptions(rawOptions, context);
|
|
1202
|
+
const args = buildCfSshArgs(appName, options.target, [
|
|
1203
|
+
"--disable-pseudo-tty",
|
|
1204
|
+
"-c",
|
|
1205
|
+
command
|
|
1206
|
+
]);
|
|
1207
|
+
return await runSshOneShot(args, context, options);
|
|
1208
|
+
}
|
|
1209
|
+
function resolveSshOptions(raw, context) {
|
|
1210
|
+
const input = typeof raw === "number" ? { timeoutMs: raw } : raw;
|
|
1211
|
+
const requestedTimeoutMs = input.timeoutMs ?? DEFAULT_CF_COMMAND_TIMEOUT_MS;
|
|
1212
|
+
const maxOutputBytes = input.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
1213
|
+
if (!Number.isSafeInteger(requestedTimeoutMs) || requestedTimeoutMs <= 0) {
|
|
1214
|
+
throw new CfDebuggerError("UNSAFE_INPUT", "timeoutMs must be a positive safe integer.");
|
|
1215
|
+
}
|
|
1216
|
+
if (!Number.isSafeInteger(maxOutputBytes) || maxOutputBytes <= 0) {
|
|
1217
|
+
throw new CfDebuggerError("UNSAFE_INPUT", "maxOutputBytes must be a positive safe integer.");
|
|
1218
|
+
}
|
|
1219
|
+
const remainingMs = context.deadlineAt === void 0 ? requestedTimeoutMs : Math.max(1, context.deadlineAt - Date.now());
|
|
1220
|
+
return {
|
|
1221
|
+
target: input,
|
|
1222
|
+
timeoutMs: Math.min(requestedTimeoutMs, remainingMs),
|
|
1223
|
+
maxOutputBytes
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
function createResult(exitCode, state) {
|
|
1227
|
+
const stdout = safeOutputText(state.stdout, state.redaction, state.outputLimit, "head");
|
|
1228
|
+
const stderr = safeOutputText(state.stderr, state.redaction, state.outputLimit, "head");
|
|
1229
|
+
return {
|
|
1230
|
+
exitCode,
|
|
1231
|
+
stdout,
|
|
1232
|
+
stderr,
|
|
1233
|
+
outputTruncated: state.stdout.truncated || state.stderr.truncated,
|
|
1234
|
+
stdoutTruncated: state.stdout.truncated,
|
|
1235
|
+
stderrTruncated: state.stderr.truncated
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
function createSshExecutionState(context, outputLimit) {
|
|
1239
|
+
const redaction = createRedactionPolicy(context.sensitiveValues ?? []);
|
|
1240
|
+
return {
|
|
1241
|
+
stdout: createBoundedOutput(),
|
|
1242
|
+
stderr: createBoundedOutput(),
|
|
1243
|
+
outputLimit,
|
|
1244
|
+
retainedLimit: outputLimit + redaction.overlapBytes,
|
|
1245
|
+
redaction,
|
|
1246
|
+
settled: false,
|
|
1247
|
+
aborted: false,
|
|
1248
|
+
timedOut: false,
|
|
1249
|
+
terminationStarted: false
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
function childIsOpen(child) {
|
|
1253
|
+
return child.exitCode === null && child.signalCode === null;
|
|
1254
|
+
}
|
|
1255
|
+
async function captureSshChildIdentity(child) {
|
|
1256
|
+
if (nodeProcess3.platform === "win32" || child.pid === void 0) {
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
try {
|
|
1260
|
+
return await readProcessIdentity(child.pid);
|
|
1261
|
+
} catch {
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
async function canSignalSshChild(child, childIdentity, signal) {
|
|
1266
|
+
if (!childIsOpen(child) || child.pid === void 0) {
|
|
1267
|
+
return false;
|
|
1268
|
+
}
|
|
1269
|
+
const expectedIdentity = await childIdentity;
|
|
1270
|
+
if (expectedIdentity === void 0) {
|
|
1271
|
+
return signal === "SIGTERM" && childIsOpen(child);
|
|
1272
|
+
}
|
|
1273
|
+
return await inspectProcessIdentity(child.pid, expectedIdentity) === "match" && childIsOpen(child);
|
|
1274
|
+
}
|
|
1275
|
+
function abandonUnverifiedSshChild(child) {
|
|
1276
|
+
try {
|
|
1277
|
+
child.stdout?.destroy();
|
|
1278
|
+
} catch {
|
|
1279
|
+
}
|
|
1280
|
+
try {
|
|
1281
|
+
child.stderr?.destroy();
|
|
1282
|
+
} catch {
|
|
1283
|
+
}
|
|
1284
|
+
try {
|
|
1285
|
+
child.unref();
|
|
1286
|
+
} catch {
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
function terminateSshExecution(child, childIdentity, state, settle) {
|
|
1290
|
+
if (state.settled || state.terminationStarted) {
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
state.terminationStarted = true;
|
|
1294
|
+
const settleUnverified = () => {
|
|
1295
|
+
if (!state.settled) {
|
|
1296
|
+
abandonUnverifiedSshChild(child);
|
|
1297
|
+
settle(createResult(null, state));
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
void (async () => {
|
|
1301
|
+
if (!await canSignalSshChild(child, childIdentity, "SIGTERM") || state.settled) {
|
|
1302
|
+
settleUnverified();
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
signalChild(child, "SIGTERM");
|
|
1306
|
+
state.forceKillTimer = setTimeout(() => {
|
|
1307
|
+
void (async () => {
|
|
1308
|
+
if (!await canSignalSshChild(child, childIdentity, "SIGKILL") || state.settled) {
|
|
1309
|
+
settleUnverified();
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
signalChild(child, "SIGKILL");
|
|
1313
|
+
})().catch(settleUnverified);
|
|
1314
|
+
}, 1e3);
|
|
1315
|
+
})().catch(settleUnverified);
|
|
1316
|
+
}
|
|
1317
|
+
function createSshSettler(state, options, context, onAbort, resolve, reject) {
|
|
1318
|
+
return (result) => {
|
|
1319
|
+
if (state.settled) {
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
state.settled = true;
|
|
1323
|
+
clearTimeout(state.timeoutTimer);
|
|
1324
|
+
clearTimeout(state.forceKillTimer);
|
|
1325
|
+
context.signal?.removeEventListener("abort", onAbort);
|
|
1326
|
+
if (state.aborted) {
|
|
1327
|
+
reject(sshAbortError(context));
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
resolve(state.timedOut ? { ...result, timedOutAfterMs: options.timeoutMs } : result);
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
function attachSshExecution(child, childIdentity, context, options, resolve, reject) {
|
|
1334
|
+
const state = createSshExecutionState(context, options.maxOutputBytes);
|
|
1335
|
+
const onAbort = () => {
|
|
1336
|
+
state.aborted = true;
|
|
1337
|
+
terminateSshExecution(child, childIdentity, state, settle);
|
|
1338
|
+
};
|
|
1339
|
+
const settle = createSshSettler(state, options, context, onAbort, resolve, reject);
|
|
1340
|
+
state.timeoutTimer = setTimeout(() => {
|
|
1341
|
+
state.timedOut = true;
|
|
1342
|
+
terminateSshExecution(child, childIdentity, state, settle);
|
|
1343
|
+
}, options.timeoutMs);
|
|
1344
|
+
if (context.signal?.aborted) {
|
|
1345
|
+
onAbort();
|
|
1346
|
+
} else {
|
|
1347
|
+
context.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1348
|
+
}
|
|
1349
|
+
child.stdout.on("data", (data) => {
|
|
1350
|
+
appendHead(state.stdout, data, state.outputLimit, state.retainedLimit);
|
|
1351
|
+
});
|
|
1352
|
+
child.stderr.on("data", (data) => {
|
|
1353
|
+
appendHead(state.stderr, data, state.outputLimit, state.retainedLimit);
|
|
1354
|
+
});
|
|
1355
|
+
child.on("close", (code, signal) => {
|
|
1356
|
+
const base = createResult(code, state);
|
|
1357
|
+
settle(state.timedOut || signal === null ? base : { ...base, signal });
|
|
1358
|
+
});
|
|
1359
|
+
child.on("error", (error) => {
|
|
1360
|
+
appendHead(state.stderr, error.message, state.outputLimit, state.retainedLimit);
|
|
1361
|
+
settle(createResult(null, state));
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
function runSshOneShot(args, context, options) {
|
|
1365
|
+
if (context.signal?.aborted || isDeadlineExpired(context)) {
|
|
1366
|
+
return Promise.reject(sshAbortError(context));
|
|
1367
|
+
}
|
|
1368
|
+
return new Promise((resolve, reject) => {
|
|
1369
|
+
const child = spawn(resolveBin(context), [...args], {
|
|
1370
|
+
env: buildEnv(context.cfHome),
|
|
1371
|
+
detached: nodeProcess3.platform !== "win32",
|
|
1372
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1373
|
+
});
|
|
1374
|
+
const childIdentity = captureSshChildIdentity(child);
|
|
1375
|
+
attachSshExecution(child, childIdentity, context, options, resolve, reject);
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
function signalChild(child, signal) {
|
|
1379
|
+
if (nodeProcess3.platform !== "win32" && child.pid !== void 0) {
|
|
1380
|
+
try {
|
|
1381
|
+
nodeProcess3.kill(-child.pid, signal);
|
|
1382
|
+
return;
|
|
1383
|
+
} catch {
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
try {
|
|
1387
|
+
child.kill(signal);
|
|
1388
|
+
} catch {
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// src/cloud-foundry/ssh.ts
|
|
1393
|
+
var tunnelCaptures = /* @__PURE__ */ new WeakMap();
|
|
1083
1394
|
function isSshDisabledError(stderr) {
|
|
1084
1395
|
return stderr.toLowerCase().includes("ssh support is disabled");
|
|
1085
1396
|
}
|
|
@@ -1244,21 +1555,41 @@ function spawnSshTunnel(appName, localPort, remotePort, context, target = {}) {
|
|
|
1244
1555
|
}
|
|
1245
1556
|
const tunnelArg = `${localPort.toString()}:localhost:${remotePort.toString()}`;
|
|
1246
1557
|
const args = buildCfSshArgs(appName, target, ["-N", "-L", tunnelArg]);
|
|
1247
|
-
const child =
|
|
1558
|
+
const child = spawn2(resolveBin(context), [...args], {
|
|
1248
1559
|
env: buildEnv(context.cfHome),
|
|
1249
|
-
detached:
|
|
1560
|
+
detached: nodeProcess4.platform !== "win32",
|
|
1250
1561
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1251
1562
|
});
|
|
1252
1563
|
attachTunnelCapture(child, context);
|
|
1253
1564
|
return child;
|
|
1254
1565
|
}
|
|
1255
1566
|
|
|
1567
|
+
// src/restart-policy.ts
|
|
1568
|
+
function parseRestartEnvironment(value) {
|
|
1569
|
+
if (value === void 0) {
|
|
1570
|
+
return "unset";
|
|
1571
|
+
}
|
|
1572
|
+
if (value === "0") {
|
|
1573
|
+
return "forbid";
|
|
1574
|
+
}
|
|
1575
|
+
if (value === "1") {
|
|
1576
|
+
return "allow";
|
|
1577
|
+
}
|
|
1578
|
+
throw new CfDebuggerError(
|
|
1579
|
+
"UNSAFE_INPUT",
|
|
1580
|
+
"CF_DEBUGGER_ALLOW_RESTART must be 0 or 1."
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
function applyRestartEnvironmentVeto(requested, value) {
|
|
1584
|
+
return parseRestartEnvironment(value) === "forbid" ? false : requested === true;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1256
1587
|
// src/session-state/store.ts
|
|
1257
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
1258
|
-
import { chmod as chmod3, mkdir as mkdir3, readFile as
|
|
1588
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
1589
|
+
import { chmod as chmod3, mkdir as mkdir3, readFile as readFile5, rename, unlink as unlink3, writeFile as writeFile2 } from "fs/promises";
|
|
1259
1590
|
import { hostname as getHostname2 } from "os";
|
|
1260
1591
|
import { dirname as dirname2, isAbsolute as isAbsolute3 } from "path";
|
|
1261
|
-
import
|
|
1592
|
+
import nodeProcess6 from "process";
|
|
1262
1593
|
|
|
1263
1594
|
// src/debug-session/constants.ts
|
|
1264
1595
|
var DEFAULT_TUNNEL_READY_TIMEOUT_MS = 18e4;
|
|
@@ -1269,87 +1600,6 @@ var CHILD_SIGTERM_GRACE_MS = 2e3;
|
|
|
1269
1600
|
var CHILD_SIGKILL_GRACE_MS = 1e3;
|
|
1270
1601
|
var PID_TERMINATION_POLL_MS = 100;
|
|
1271
1602
|
|
|
1272
|
-
// src/debug-session/process-identity.ts
|
|
1273
|
-
import { execFile as execFile2 } from "child_process";
|
|
1274
|
-
import { readFile } from "fs/promises";
|
|
1275
|
-
import nodeProcess3 from "process";
|
|
1276
|
-
import { promisify } from "util";
|
|
1277
|
-
var execFileAsync = promisify(execFile2);
|
|
1278
|
-
function errorCode(error) {
|
|
1279
|
-
if (typeof error !== "object" || error === null) {
|
|
1280
|
-
return void 0;
|
|
1281
|
-
}
|
|
1282
|
-
const code = Reflect.get(error, "code");
|
|
1283
|
-
return typeof code === "string" ? code : void 0;
|
|
1284
|
-
}
|
|
1285
|
-
function parseLinuxProcessStartTime(statLine) {
|
|
1286
|
-
const commandEnd = statLine.lastIndexOf(")");
|
|
1287
|
-
if (commandEnd < 0) {
|
|
1288
|
-
return void 0;
|
|
1289
|
-
}
|
|
1290
|
-
const fieldsAfterCommand = statLine.slice(commandEnd + 1).trim().split(/\s+/);
|
|
1291
|
-
const startTime = fieldsAfterCommand[19];
|
|
1292
|
-
return startTime !== void 0 && /^\d+$/.test(startTime) ? startTime : void 0;
|
|
1293
|
-
}
|
|
1294
|
-
function throwIfAborted(signal) {
|
|
1295
|
-
if (signal?.aborted) {
|
|
1296
|
-
throw new CfDebuggerError("ABORTED", "Process identity inspection was aborted.");
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
async function linuxProcessIdentity(pid, signal) {
|
|
1300
|
-
try {
|
|
1301
|
-
const statLine = await readFile(`/proc/${pid.toString()}/stat`, {
|
|
1302
|
-
encoding: "utf8",
|
|
1303
|
-
...signal === void 0 ? {} : { signal }
|
|
1304
|
-
});
|
|
1305
|
-
const startTime = parseLinuxProcessStartTime(statLine);
|
|
1306
|
-
return startTime === void 0 ? void 0 : `linux:${startTime}`;
|
|
1307
|
-
} catch (error) {
|
|
1308
|
-
throwIfAborted(signal);
|
|
1309
|
-
if (errorCode(error) === "ENOENT") {
|
|
1310
|
-
return void 0;
|
|
1311
|
-
}
|
|
1312
|
-
return void 0;
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
async function darwinProcessIdentity(pid, signal) {
|
|
1316
|
-
try {
|
|
1317
|
-
const { stdout } = await execFileAsync("ps", ["-p", pid.toString(), "-o", "lstart="], {
|
|
1318
|
-
...signal === void 0 ? {} : { signal },
|
|
1319
|
-
timeout: 2e3
|
|
1320
|
-
});
|
|
1321
|
-
const startedAt = stdout.trim();
|
|
1322
|
-
return startedAt.length === 0 ? void 0 : `darwin:${startedAt}`;
|
|
1323
|
-
} catch {
|
|
1324
|
-
throwIfAborted(signal);
|
|
1325
|
-
return void 0;
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
async function readProcessIdentity(pid, signal) {
|
|
1329
|
-
throwIfAborted(signal);
|
|
1330
|
-
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
1331
|
-
return void 0;
|
|
1332
|
-
}
|
|
1333
|
-
if (nodeProcess3.platform === "linux") {
|
|
1334
|
-
return await linuxProcessIdentity(pid, signal);
|
|
1335
|
-
}
|
|
1336
|
-
if (nodeProcess3.platform === "darwin") {
|
|
1337
|
-
return await darwinProcessIdentity(pid, signal);
|
|
1338
|
-
}
|
|
1339
|
-
return void 0;
|
|
1340
|
-
}
|
|
1341
|
-
async function inspectProcessIdentity(pid, expected, signal) {
|
|
1342
|
-
throwIfAborted(signal);
|
|
1343
|
-
if (expected === void 0) {
|
|
1344
|
-
return "match";
|
|
1345
|
-
}
|
|
1346
|
-
const current = await readProcessIdentity(pid, signal);
|
|
1347
|
-
if (current === void 0) {
|
|
1348
|
-
return "unavailable";
|
|
1349
|
-
}
|
|
1350
|
-
return current === expected ? "match" : "mismatch";
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
1603
|
// src/lock.ts
|
|
1354
1604
|
import { randomUUID } from "crypto";
|
|
1355
1605
|
import { chmod, mkdir, open, readFile as readFile2, stat, unlink } from "fs/promises";
|
|
@@ -1438,18 +1688,7 @@ async function readLockOwner(lockPath) {
|
|
|
1438
1688
|
throw error;
|
|
1439
1689
|
}
|
|
1440
1690
|
}
|
|
1441
|
-
async function
|
|
1442
|
-
let modifiedAt2;
|
|
1443
|
-
try {
|
|
1444
|
-
modifiedAt2 = (await stat(lockPath)).mtimeMs;
|
|
1445
|
-
} catch (error) {
|
|
1446
|
-
if (errorCode2(error) === "ENOENT") {
|
|
1447
|
-
return true;
|
|
1448
|
-
}
|
|
1449
|
-
throw error;
|
|
1450
|
-
}
|
|
1451
|
-
const owner = await readLockOwner(lockPath);
|
|
1452
|
-
const ageMs = Date.now() - modifiedAt2;
|
|
1691
|
+
async function lockOwnerIsStale(owner, ageMs, staleMs) {
|
|
1453
1692
|
if (owner?.hostname === hostname()) {
|
|
1454
1693
|
if (!isProcessAlive(owner.pid)) {
|
|
1455
1694
|
return true;
|
|
@@ -1470,20 +1709,54 @@ async function isStaleLock(lockPath, staleMs) {
|
|
|
1470
1709
|
}
|
|
1471
1710
|
return ageMs > staleMs * FOREIGN_HOST_STALE_MULTIPLIER;
|
|
1472
1711
|
}
|
|
1473
|
-
async function
|
|
1474
|
-
|
|
1475
|
-
|
|
1712
|
+
async function inspectLockStaleness(lockPath, staleMs) {
|
|
1713
|
+
const owner = await readLockOwner(lockPath);
|
|
1714
|
+
try {
|
|
1715
|
+
const stats = await stat(lockPath);
|
|
1716
|
+
return {
|
|
1717
|
+
fingerprint: lockFingerprint(stats),
|
|
1718
|
+
owner,
|
|
1719
|
+
stale: await lockOwnerIsStale(owner, Date.now() - stats.mtimeMs, staleMs),
|
|
1720
|
+
status: "present"
|
|
1721
|
+
};
|
|
1722
|
+
} catch (error) {
|
|
1723
|
+
if (errorCode2(error) === "ENOENT") {
|
|
1724
|
+
return { status: "missing" };
|
|
1725
|
+
}
|
|
1726
|
+
throw error;
|
|
1476
1727
|
}
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1728
|
+
}
|
|
1729
|
+
function lockFingerprint(stats) {
|
|
1730
|
+
return [
|
|
1731
|
+
stats.dev.toString(),
|
|
1732
|
+
stats.ino.toString(),
|
|
1733
|
+
stats.size.toString(),
|
|
1734
|
+
stats.mtimeMs.toString()
|
|
1735
|
+
].join(":");
|
|
1736
|
+
}
|
|
1737
|
+
async function removeObservedLock(lockPath, observation) {
|
|
1738
|
+
if (observation.owner !== void 0) {
|
|
1739
|
+
return await removeOwnedLock(lockPath, observation.owner.token);
|
|
1481
1740
|
}
|
|
1482
|
-
|
|
1483
|
-
if (
|
|
1484
|
-
|
|
1741
|
+
try {
|
|
1742
|
+
if (lockFingerprint(await stat(lockPath)) !== observation.fingerprint) {
|
|
1743
|
+
return false;
|
|
1485
1744
|
}
|
|
1486
|
-
|
|
1745
|
+
await unlink(lockPath);
|
|
1746
|
+
return true;
|
|
1747
|
+
} catch (error) {
|
|
1748
|
+
if (errorCode2(error) === "ENOENT") {
|
|
1749
|
+
return false;
|
|
1750
|
+
}
|
|
1751
|
+
throw error;
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
async function reclaimAbandonedRecoveryLock(recoveryPath, staleMs) {
|
|
1755
|
+
const observation = await inspectLockStaleness(recoveryPath, staleMs);
|
|
1756
|
+
if (observation.status !== "present" || !observation.stale || observation.owner === void 0) {
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
await removeOwnedLock(recoveryPath, observation.owner.token);
|
|
1487
1760
|
}
|
|
1488
1761
|
async function reclaimStaleLock(lockPath, staleMs) {
|
|
1489
1762
|
const recoveryPath = `${lockPath}.recovery`;
|
|
@@ -1498,17 +1771,11 @@ async function reclaimStaleLock(lockPath, staleMs) {
|
|
|
1498
1771
|
throw error;
|
|
1499
1772
|
}
|
|
1500
1773
|
try {
|
|
1501
|
-
|
|
1774
|
+
const observation = await inspectLockStaleness(lockPath, staleMs);
|
|
1775
|
+
if (observation.status !== "present" || !observation.stale) {
|
|
1502
1776
|
return false;
|
|
1503
1777
|
}
|
|
1504
|
-
|
|
1505
|
-
await unlink(lockPath);
|
|
1506
|
-
} catch (error) {
|
|
1507
|
-
if (errorCode2(error) !== "ENOENT") {
|
|
1508
|
-
throw error;
|
|
1509
|
-
}
|
|
1510
|
-
}
|
|
1511
|
-
return true;
|
|
1778
|
+
return await removeObservedLock(lockPath, observation);
|
|
1512
1779
|
} finally {
|
|
1513
1780
|
await releaseFileLock(recoveryPath, recoveryLock);
|
|
1514
1781
|
}
|
|
@@ -1516,12 +1783,13 @@ async function reclaimStaleLock(lockPath, staleMs) {
|
|
|
1516
1783
|
async function removeOwnedLock(lockPath, token) {
|
|
1517
1784
|
const owner = await readLockOwner(lockPath);
|
|
1518
1785
|
if (owner?.token !== token) {
|
|
1519
|
-
return;
|
|
1786
|
+
return false;
|
|
1520
1787
|
}
|
|
1521
|
-
await unlink(lockPath).catch((error) => {
|
|
1788
|
+
return await unlink(lockPath).then(() => true).catch((error) => {
|
|
1522
1789
|
if (errorCode2(error) !== "ENOENT") {
|
|
1523
1790
|
throw error;
|
|
1524
1791
|
}
|
|
1792
|
+
return false;
|
|
1525
1793
|
});
|
|
1526
1794
|
}
|
|
1527
1795
|
async function createFileLock(lockPath) {
|
|
@@ -1794,16 +2062,24 @@ function decodeSession(value) {
|
|
|
1794
2062
|
return void 0;
|
|
1795
2063
|
}
|
|
1796
2064
|
}
|
|
1797
|
-
function
|
|
2065
|
+
function decodeSessions(rawSessions) {
|
|
1798
2066
|
const seen = /* @__PURE__ */ new Set();
|
|
1799
|
-
const
|
|
1800
|
-
|
|
2067
|
+
const sessions = [];
|
|
2068
|
+
const dropped = [];
|
|
2069
|
+
for (const [index, raw] of rawSessions.entries()) {
|
|
2070
|
+
const session = decodeSession(raw);
|
|
2071
|
+
if (session === void 0) {
|
|
2072
|
+
dropped.push(`session[${index.toString()}]: invalid entry`);
|
|
2073
|
+
continue;
|
|
2074
|
+
}
|
|
1801
2075
|
if (seen.has(session.sessionId)) {
|
|
1802
|
-
|
|
2076
|
+
dropped.push(`session[${index.toString()}]: duplicate sessionId ${session.sessionId}`);
|
|
2077
|
+
continue;
|
|
1803
2078
|
}
|
|
1804
2079
|
seen.add(session.sessionId);
|
|
2080
|
+
sessions.push(session);
|
|
1805
2081
|
}
|
|
1806
|
-
return
|
|
2082
|
+
return { dropped, sessions };
|
|
1807
2083
|
}
|
|
1808
2084
|
function decodeStateFileDetailed(value) {
|
|
1809
2085
|
if (typeof value !== "object" || value === null) {
|
|
@@ -1816,36 +2092,46 @@ function decodeStateFileDetailed(value) {
|
|
|
1816
2092
|
if (!Array.isArray(rawSessions)) {
|
|
1817
2093
|
return { kind: "invalid-file", reason: "sessions is not an array" };
|
|
1818
2094
|
}
|
|
1819
|
-
const decoded = rawSessions
|
|
1820
|
-
const valid = decoded.filter((session) => session !== void 0);
|
|
1821
|
-
const duplicates = duplicateSessionIds(valid);
|
|
1822
|
-
const sessions = valid.filter((session) => !duplicates.has(session.sessionId));
|
|
1823
|
-
const dropped = decoded.flatMap((session, index) => {
|
|
1824
|
-
if (session === void 0) {
|
|
1825
|
-
return [`session[${index.toString()}]: invalid entry`];
|
|
1826
|
-
}
|
|
1827
|
-
return duplicates.has(session.sessionId) ? [`session[${index.toString()}]: duplicate sessionId ${session.sessionId}`] : [];
|
|
1828
|
-
});
|
|
2095
|
+
const decoded = decodeSessions(rawSessions);
|
|
1829
2096
|
return {
|
|
1830
2097
|
kind: "decoded",
|
|
1831
|
-
state: { version: "2", sessions },
|
|
1832
|
-
dropped
|
|
2098
|
+
state: { version: "2", sessions: decoded.sessions },
|
|
2099
|
+
dropped: decoded.dropped
|
|
1833
2100
|
};
|
|
1834
2101
|
}
|
|
1835
2102
|
|
|
1836
2103
|
// src/session-state/health.ts
|
|
1837
2104
|
import { hostname as getHostname } from "os";
|
|
1838
|
-
import
|
|
2105
|
+
import nodeProcess5 from "process";
|
|
2106
|
+
|
|
2107
|
+
// src/debug-session/session-process.ts
|
|
2108
|
+
function startupAgeLimit(session) {
|
|
2109
|
+
return (session.startupTimeoutMs ?? MAX_STARTUP_TIMEOUT_MS) + STARTUP_STALE_SLACK_MS;
|
|
2110
|
+
}
|
|
2111
|
+
function startupExpired(session) {
|
|
2112
|
+
const startedAt = Date.parse(session.startedAt);
|
|
2113
|
+
return Number.isNaN(startedAt) || Date.now() - startedAt > startupAgeLimit(session);
|
|
2114
|
+
}
|
|
2115
|
+
async function inspectRecordedProcess(pid, identity, isAlive, signal) {
|
|
2116
|
+
if (signal?.aborted) {
|
|
2117
|
+
throw new CfDebuggerError("ABORTED", "Session health inspection was aborted.");
|
|
2118
|
+
}
|
|
2119
|
+
if (!isAlive(pid)) {
|
|
2120
|
+
return "dead";
|
|
2121
|
+
}
|
|
2122
|
+
return await inspectProcessIdentity(pid, identity, signal);
|
|
2123
|
+
}
|
|
1839
2124
|
|
|
1840
2125
|
// src/network/ports.ts
|
|
1841
2126
|
import { execFile as execFile3 } from "child_process";
|
|
1842
2127
|
import { readdir, readFile as readFile3, readlink } from "fs/promises";
|
|
1843
2128
|
import { get as httpGet } from "http";
|
|
1844
|
-
import {
|
|
2129
|
+
import { createServer, Socket } from "net";
|
|
1845
2130
|
import { promisify as promisify2 } from "util";
|
|
1846
2131
|
var execFileAsync2 = promisify2(execFile3);
|
|
1847
2132
|
var PROBE_INTERVAL_MS = 250;
|
|
1848
|
-
var
|
|
2133
|
+
var INITIAL_INSPECTOR_ATTEMPT_TIMEOUT_MS = 2500;
|
|
2134
|
+
var MAX_INSPECTOR_ATTEMPT_TIMEOUT_MS = 1e4;
|
|
1849
2135
|
var MAX_INSPECTOR_RESPONSE_BYTES = 64 * 1024;
|
|
1850
2136
|
var OWNER_COMMAND_TIMEOUT_MS = 5e3;
|
|
1851
2137
|
function sortedUniquePids(pids) {
|
|
@@ -2101,22 +2387,32 @@ async function isPortFree(port, signal) {
|
|
|
2101
2387
|
}
|
|
2102
2388
|
async function isPortListening(port, timeoutMs = 200) {
|
|
2103
2389
|
return await new Promise((resolve) => {
|
|
2104
|
-
const socket =
|
|
2105
|
-
|
|
2106
|
-
|
|
2390
|
+
const socket = new Socket();
|
|
2391
|
+
let settled = false;
|
|
2392
|
+
const finish = (listening) => {
|
|
2393
|
+
if (settled) {
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
settled = true;
|
|
2107
2397
|
socket.destroy();
|
|
2108
|
-
resolve(
|
|
2398
|
+
resolve(listening);
|
|
2399
|
+
};
|
|
2400
|
+
socket.once("connect", () => {
|
|
2401
|
+
finish(true);
|
|
2109
2402
|
});
|
|
2110
2403
|
socket.once("error", () => {
|
|
2111
|
-
|
|
2112
|
-
resolve(false);
|
|
2404
|
+
finish(false);
|
|
2113
2405
|
});
|
|
2114
2406
|
socket.once("timeout", () => {
|
|
2115
|
-
|
|
2116
|
-
resolve(false);
|
|
2407
|
+
finish(false);
|
|
2117
2408
|
});
|
|
2409
|
+
socket.setTimeout(positiveTimeout(timeoutMs));
|
|
2410
|
+
socket.connect({ port, host: "127.0.0.1" });
|
|
2118
2411
|
});
|
|
2119
2412
|
}
|
|
2413
|
+
function positiveTimeout(timeoutMs) {
|
|
2414
|
+
return Number.isFinite(timeoutMs) ? Math.max(1, Math.floor(timeoutMs)) : 1;
|
|
2415
|
+
}
|
|
2120
2416
|
function throwIfAborted2(signal) {
|
|
2121
2417
|
if (signal?.aborted) {
|
|
2122
2418
|
throw new CfDebuggerError("ABORTED", "Operation aborted by caller");
|
|
@@ -2144,7 +2440,7 @@ async function probeTunnelReady(port, timeoutMs, signal) {
|
|
|
2144
2440
|
throwIfAborted2(signal);
|
|
2145
2441
|
while (Date.now() < deadline) {
|
|
2146
2442
|
const remainingMs = deadline - Date.now();
|
|
2147
|
-
if (await isPortListening(port, Math.min(200, remainingMs))) {
|
|
2443
|
+
if (await isPortListening(port, positiveTimeout(Math.min(200, remainingMs)))) {
|
|
2148
2444
|
return true;
|
|
2149
2445
|
}
|
|
2150
2446
|
const waitMs = Math.min(PROBE_INTERVAL_MS, Math.max(0, deadline - Date.now()));
|
|
@@ -2213,21 +2509,28 @@ function readInspectorResponse(response, finish) {
|
|
|
2213
2509
|
finish(false);
|
|
2214
2510
|
});
|
|
2215
2511
|
}
|
|
2512
|
+
function finishInspectorAttempt(state, ready, resolve) {
|
|
2513
|
+
if (state.settled) {
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
state.settled = true;
|
|
2517
|
+
clearTimeout(state.timer);
|
|
2518
|
+
state.removeAbortListener?.();
|
|
2519
|
+
resolve(ready);
|
|
2520
|
+
}
|
|
2216
2521
|
function probeInspectorAttempt(port, timeoutMs, signal) {
|
|
2217
2522
|
throwIfAborted2(signal);
|
|
2218
2523
|
return new Promise((resolve, reject) => {
|
|
2219
2524
|
const state = { settled: false };
|
|
2220
2525
|
const finish = (ready) => {
|
|
2221
|
-
|
|
2222
|
-
state.settled = true;
|
|
2223
|
-
clearTimeout(state.timer);
|
|
2224
|
-
state.removeAbortListener?.();
|
|
2225
|
-
resolve(ready);
|
|
2226
|
-
}
|
|
2526
|
+
finishInspectorAttempt(state, ready, resolve);
|
|
2227
2527
|
};
|
|
2228
|
-
const request = httpGet(
|
|
2229
|
-
|
|
2230
|
-
|
|
2528
|
+
const request = httpGet(
|
|
2529
|
+
{ agent: false, host: "127.0.0.1", port, path: "/json/list" },
|
|
2530
|
+
(response) => {
|
|
2531
|
+
readInspectorResponse(response, finish);
|
|
2532
|
+
}
|
|
2533
|
+
);
|
|
2231
2534
|
const onAbort = () => {
|
|
2232
2535
|
request.destroy();
|
|
2233
2536
|
if (!state.settled) {
|
|
@@ -2248,7 +2551,7 @@ function probeInspectorAttempt(port, timeoutMs, signal) {
|
|
|
2248
2551
|
state.timer = setTimeout(() => {
|
|
2249
2552
|
request.destroy();
|
|
2250
2553
|
finish(false);
|
|
2251
|
-
}, timeoutMs);
|
|
2554
|
+
}, positiveTimeout(timeoutMs));
|
|
2252
2555
|
request.once("error", () => {
|
|
2253
2556
|
if (signal?.aborted) {
|
|
2254
2557
|
onAbort();
|
|
@@ -2258,18 +2561,25 @@ function probeInspectorAttempt(port, timeoutMs, signal) {
|
|
|
2258
2561
|
});
|
|
2259
2562
|
});
|
|
2260
2563
|
}
|
|
2564
|
+
function inspectorAttemptTimeout(attempt, remainingMs) {
|
|
2565
|
+
const growingCap = Math.min(
|
|
2566
|
+
MAX_INSPECTOR_ATTEMPT_TIMEOUT_MS,
|
|
2567
|
+
INITIAL_INSPECTOR_ATTEMPT_TIMEOUT_MS * 2 ** attempt
|
|
2568
|
+
);
|
|
2569
|
+
const retryShare = Math.ceil(remainingMs / 2);
|
|
2570
|
+
return positiveTimeout(Math.min(growingCap, retryShare, remainingMs));
|
|
2571
|
+
}
|
|
2261
2572
|
async function probeInspectorReady(port, timeoutMs, signal) {
|
|
2262
2573
|
const deadline = Date.now() + timeoutMs;
|
|
2574
|
+
let attempt = 0;
|
|
2263
2575
|
throwIfAborted2(signal);
|
|
2264
2576
|
while (Date.now() < deadline) {
|
|
2265
2577
|
const remainingMs = deadline - Date.now();
|
|
2266
|
-
const attemptTimeoutMs =
|
|
2267
|
-
1,
|
|
2268
|
-
Math.min(INSPECTOR_ATTEMPT_TIMEOUT_MS, remainingMs)
|
|
2269
|
-
);
|
|
2578
|
+
const attemptTimeoutMs = inspectorAttemptTimeout(attempt, remainingMs);
|
|
2270
2579
|
if (await probeInspectorAttempt(port, attemptTimeoutMs, signal)) {
|
|
2271
2580
|
return { status: "ready" };
|
|
2272
2581
|
}
|
|
2582
|
+
attempt += 1;
|
|
2273
2583
|
const waitMs = Math.min(PROBE_INTERVAL_MS, Math.max(0, deadline - Date.now()));
|
|
2274
2584
|
if (waitMs > 0) {
|
|
2275
2585
|
await waitForNextProbe(waitMs, signal);
|
|
@@ -2289,34 +2599,18 @@ function errorCode4(error) {
|
|
|
2289
2599
|
}
|
|
2290
2600
|
function isPidAlive(pid) {
|
|
2291
2601
|
try {
|
|
2292
|
-
|
|
2602
|
+
nodeProcess5.kill(pid, 0);
|
|
2293
2603
|
return true;
|
|
2294
2604
|
} catch (error) {
|
|
2295
2605
|
return errorCode4(error) !== "ESRCH";
|
|
2296
2606
|
}
|
|
2297
2607
|
}
|
|
2298
2608
|
function isProcessGroupAlive(pid) {
|
|
2299
|
-
return
|
|
2609
|
+
return nodeProcess5.platform !== "win32" && isPidAlive(-pid);
|
|
2300
2610
|
}
|
|
2301
2611
|
function isPidOrGroupAlive(pid) {
|
|
2302
2612
|
return isPidAlive(pid) || isProcessGroupAlive(pid);
|
|
2303
2613
|
}
|
|
2304
|
-
function startupAgeLimit(session) {
|
|
2305
|
-
return (session.startupTimeoutMs ?? MAX_STARTUP_TIMEOUT_MS) + STARTUP_STALE_SLACK_MS;
|
|
2306
|
-
}
|
|
2307
|
-
function startupExpired(session) {
|
|
2308
|
-
const startedAt = Date.parse(session.startedAt);
|
|
2309
|
-
return Number.isNaN(startedAt) || Date.now() - startedAt > startupAgeLimit(session);
|
|
2310
|
-
}
|
|
2311
|
-
async function inspectRecordedProcess(pid, identity, signal) {
|
|
2312
|
-
if (signal?.aborted) {
|
|
2313
|
-
throw new CfDebuggerError("ABORTED", "Session health inspection was aborted.");
|
|
2314
|
-
}
|
|
2315
|
-
if (!isPidAlive(pid)) {
|
|
2316
|
-
return "dead";
|
|
2317
|
-
}
|
|
2318
|
-
return await inspectProcessIdentity(pid, identity, signal);
|
|
2319
|
-
}
|
|
2320
2614
|
async function readySessionHealth(session, signal) {
|
|
2321
2615
|
const tunnelPid = session.tunnelPid;
|
|
2322
2616
|
if (tunnelPid === void 0) {
|
|
@@ -2325,6 +2619,7 @@ async function readySessionHealth(session, signal) {
|
|
|
2325
2619
|
const processVerdict = await inspectRecordedProcess(
|
|
2326
2620
|
tunnelPid,
|
|
2327
2621
|
session.tunnelProcessIdentity,
|
|
2622
|
+
isPidAlive,
|
|
2328
2623
|
signal
|
|
2329
2624
|
);
|
|
2330
2625
|
if (processVerdict === "dead" && isProcessGroupAlive(tunnelPid)) {
|
|
@@ -2362,6 +2657,7 @@ async function startingSessionHealth(session, signal) {
|
|
|
2362
2657
|
const processVerdict = await inspectRecordedProcess(
|
|
2363
2658
|
controllerPid,
|
|
2364
2659
|
session.controllerProcessIdentity,
|
|
2660
|
+
isPidAlive,
|
|
2365
2661
|
signal
|
|
2366
2662
|
);
|
|
2367
2663
|
if (processVerdict === "match") {
|
|
@@ -2381,19 +2677,16 @@ async function inspectSessionHealth(session, host = getHostname(), signal) {
|
|
|
2381
2677
|
}
|
|
2382
2678
|
return session.status === "ready" ? await readySessionHealth(session, signal) : await startingSessionHealth(session, signal);
|
|
2383
2679
|
}
|
|
2384
|
-
async function filterStaleSessions(sessions, signal) {
|
|
2385
|
-
const host = getHostname();
|
|
2386
|
-
const verdicts = await Promise.all(
|
|
2387
|
-
sessions.map(async (session) => [
|
|
2388
|
-
session,
|
|
2389
|
-
await inspectSessionHealth(session, host, signal)
|
|
2390
|
-
])
|
|
2391
|
-
);
|
|
2392
|
-
return verdicts.filter(([, verdict]) => verdict.status !== "stale").map(([session]) => session);
|
|
2393
|
-
}
|
|
2394
2680
|
|
|
2395
2681
|
// src/session-state/stop-intent.ts
|
|
2396
|
-
import {
|
|
2682
|
+
import {
|
|
2683
|
+
access,
|
|
2684
|
+
chmod as chmod2,
|
|
2685
|
+
mkdir as mkdir2,
|
|
2686
|
+
readFile as readFile4,
|
|
2687
|
+
unlink as unlink2,
|
|
2688
|
+
writeFile
|
|
2689
|
+
} from "fs/promises";
|
|
2397
2690
|
function errorCode5(error) {
|
|
2398
2691
|
if (typeof error !== "object" || error === null) {
|
|
2399
2692
|
return void 0;
|
|
@@ -2428,6 +2721,29 @@ async function hasSessionStopIntent(sessionId) {
|
|
|
2428
2721
|
throw error;
|
|
2429
2722
|
}
|
|
2430
2723
|
}
|
|
2724
|
+
async function inspectSessionStateStopIntent(sessionId) {
|
|
2725
|
+
let raw;
|
|
2726
|
+
try {
|
|
2727
|
+
raw = await readFile4(stateFilePath(), "utf8");
|
|
2728
|
+
} catch (error) {
|
|
2729
|
+
return errorCode5(error) === "ENOENT" ? "missing" : "unavailable";
|
|
2730
|
+
}
|
|
2731
|
+
let parsed;
|
|
2732
|
+
try {
|
|
2733
|
+
parsed = JSON.parse(raw);
|
|
2734
|
+
} catch {
|
|
2735
|
+
return "unavailable";
|
|
2736
|
+
}
|
|
2737
|
+
const decoded = decodeStateFileDetailed(parsed);
|
|
2738
|
+
if (decoded.kind === "invalid-file") {
|
|
2739
|
+
return "unavailable";
|
|
2740
|
+
}
|
|
2741
|
+
const session = decoded.state.sessions.find((candidate) => candidate.sessionId === sessionId);
|
|
2742
|
+
if (session === void 0) {
|
|
2743
|
+
return decoded.dropped.length > 0 ? "unavailable" : "missing";
|
|
2744
|
+
}
|
|
2745
|
+
return session.stopRequestedAt !== void 0 || session.status === "stopping" ? "requested" : "active";
|
|
2746
|
+
}
|
|
2431
2747
|
async function clearSessionStopIntent(sessionId) {
|
|
2432
2748
|
await unlink2(sessionStopIntentPath(sessionId)).catch((error) => {
|
|
2433
2749
|
if (errorCode5(error) !== "ENOENT") {
|
|
@@ -2449,7 +2765,7 @@ function errorCode6(error) {
|
|
|
2449
2765
|
async function readFileIfPresent(path) {
|
|
2450
2766
|
try {
|
|
2451
2767
|
await chmod3(path, 384);
|
|
2452
|
-
return await
|
|
2768
|
+
return await readFile5(path, "utf8");
|
|
2453
2769
|
} catch (error) {
|
|
2454
2770
|
if (errorCode6(error) === "ENOENT") {
|
|
2455
2771
|
return void 0;
|
|
@@ -2490,7 +2806,7 @@ async function preserveCorruptState(path, reason) {
|
|
|
2490
2806
|
const backup = corruptBackupPath(path);
|
|
2491
2807
|
await rename(path, backup);
|
|
2492
2808
|
await chmod3(backup, 384);
|
|
2493
|
-
|
|
2809
|
+
nodeProcess6.stderr.write(
|
|
2494
2810
|
`[cf-debugger] warning: preserved invalid state at ${backup} (${reason}).
|
|
2495
2811
|
`
|
|
2496
2812
|
);
|
|
@@ -2521,7 +2837,7 @@ async function readStateRaw() {
|
|
|
2521
2837
|
if (decoded.dropped.length > 0) {
|
|
2522
2838
|
await preserveCorruptState(path, decoded.dropped.join("; "));
|
|
2523
2839
|
await writeJsonFileAtomic(path, decoded.state);
|
|
2524
|
-
|
|
2840
|
+
nodeProcess6.stderr.write(
|
|
2525
2841
|
`[cf-debugger] warning: dropped ${decoded.dropped.length.toString()} invalid state entr${decoded.dropped.length === 1 ? "y" : "ies"}; valid sessions were retained.
|
|
2526
2842
|
`
|
|
2527
2843
|
);
|
|
@@ -2531,22 +2847,62 @@ async function readStateRaw() {
|
|
|
2531
2847
|
async function writeState(state) {
|
|
2532
2848
|
await writeJsonFileAtomic(stateFilePath(), state);
|
|
2533
2849
|
}
|
|
2534
|
-
|
|
2535
|
-
|
|
2850
|
+
function samePersistedSession(left, right) {
|
|
2851
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
2852
|
+
}
|
|
2853
|
+
async function inspectPruneCandidates(raw, signal) {
|
|
2536
2854
|
const host = getHostname2();
|
|
2537
|
-
const remote = raw.sessions.filter((session) => session.hostname !== host);
|
|
2538
2855
|
const local = raw.sessions.filter((session) => session.hostname === host);
|
|
2539
|
-
const
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2856
|
+
const verdicts = await Promise.all(local.map(async (session) => ({
|
|
2857
|
+
session,
|
|
2858
|
+
verdict: await inspectSessionHealth(session, host, signal)
|
|
2859
|
+
})));
|
|
2860
|
+
return {
|
|
2861
|
+
snapshotById: new Map(local.map((session) => [session.sessionId, session])),
|
|
2862
|
+
staleIds: new Set(
|
|
2863
|
+
verdicts.filter(({ verdict }) => verdict.status === "stale").map(({ session }) => session.sessionId)
|
|
2864
|
+
)
|
|
2865
|
+
};
|
|
2866
|
+
}
|
|
2867
|
+
async function persistPruneInspection(inspection, signal) {
|
|
2868
|
+
const raw = await readStateRaw();
|
|
2869
|
+
const host = getHostname2();
|
|
2870
|
+
const removed = [];
|
|
2871
|
+
const sessions = [];
|
|
2872
|
+
for (const session of raw.sessions.filter((entry) => entry.hostname === host)) {
|
|
2873
|
+
const snapshot = inspection.snapshotById.get(session.sessionId);
|
|
2874
|
+
if (snapshot === void 0 || !inspection.staleIds.has(session.sessionId) || !samePersistedSession(snapshot, session)) {
|
|
2875
|
+
sessions.push(session);
|
|
2876
|
+
continue;
|
|
2877
|
+
}
|
|
2878
|
+
const verdict = await inspectSessionHealth(session, host, signal);
|
|
2879
|
+
if (verdict.status === "stale") {
|
|
2880
|
+
removed.push(session);
|
|
2881
|
+
} else {
|
|
2882
|
+
sessions.push(session);
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2543
2885
|
if (removed.length > 0) {
|
|
2544
|
-
|
|
2886
|
+
const remote = raw.sessions.filter((session) => session.hostname !== host);
|
|
2887
|
+
await writeState({ version: "2", sessions: [...remote, ...sessions] });
|
|
2545
2888
|
}
|
|
2546
|
-
return { sessions, removed
|
|
2889
|
+
return { sessions, removed };
|
|
2890
|
+
}
|
|
2891
|
+
async function inspectAndPrune(options) {
|
|
2892
|
+
const snapshot = await withFileLock(
|
|
2893
|
+
stateLockPath(),
|
|
2894
|
+
readStateRaw,
|
|
2895
|
+
options
|
|
2896
|
+
);
|
|
2897
|
+
const inspection = await inspectPruneCandidates(snapshot, options?.signal);
|
|
2898
|
+
return await withFileLock(
|
|
2899
|
+
stateLockPath(),
|
|
2900
|
+
async () => await persistPruneInspection(inspection, options?.signal),
|
|
2901
|
+
options
|
|
2902
|
+
);
|
|
2547
2903
|
}
|
|
2548
2904
|
async function readActiveSessions() {
|
|
2549
|
-
const result = await
|
|
2905
|
+
const result = await inspectAndPrune();
|
|
2550
2906
|
return result.sessions;
|
|
2551
2907
|
}
|
|
2552
2908
|
async function readSessionSnapshot(options) {
|
|
@@ -2555,12 +2911,7 @@ async function readSessionSnapshot(options) {
|
|
|
2555
2911
|
}, options);
|
|
2556
2912
|
}
|
|
2557
2913
|
async function readAndPruneActiveSessions(options) {
|
|
2558
|
-
|
|
2559
|
-
stateLockPath(),
|
|
2560
|
-
async () => await readAndPruneLocked(options?.signal),
|
|
2561
|
-
options
|
|
2562
|
-
);
|
|
2563
|
-
return { sessions: result.sessions, removed: result.removed };
|
|
2914
|
+
return await inspectAndPrune(options);
|
|
2564
2915
|
}
|
|
2565
2916
|
function sessionKeyString(key) {
|
|
2566
2917
|
const base = `${key.region}:${key.org}:${key.space}:${key.app}`;
|
|
@@ -2593,34 +2944,41 @@ function matchesRegistrationTarget(session, input, target) {
|
|
|
2593
2944
|
apiEndpoint: input.apiEndpoint
|
|
2594
2945
|
});
|
|
2595
2946
|
}
|
|
2596
|
-
|
|
2947
|
+
function portCandidates(preferred, range, scanOffset) {
|
|
2597
2948
|
const candidates = preferred === void 0 ? [] : [preferred];
|
|
2598
|
-
|
|
2949
|
+
const size = range.maxPort - range.basePort + 1;
|
|
2950
|
+
for (let index = 0; index < size; index += 1) {
|
|
2951
|
+
const port = range.basePort + (scanOffset + index) % size;
|
|
2599
2952
|
if (port !== preferred) {
|
|
2600
2953
|
candidates.push(port);
|
|
2601
2954
|
}
|
|
2602
2955
|
}
|
|
2603
|
-
|
|
2956
|
+
return candidates;
|
|
2957
|
+
}
|
|
2958
|
+
async function pickPort(preferred, reserved, probe, range, scanOffset) {
|
|
2959
|
+
for (const port of portCandidates(preferred, range, scanOffset)) {
|
|
2604
2960
|
if (!reserved.has(port) && await probe(port)) {
|
|
2605
2961
|
return port;
|
|
2606
2962
|
}
|
|
2607
2963
|
}
|
|
2608
2964
|
throw new CfDebuggerError(
|
|
2609
2965
|
"PORT_UNAVAILABLE",
|
|
2610
|
-
`No free local port available in range ${basePort.toString()}\u2013${maxPort.toString()}`
|
|
2966
|
+
`No free local port available in range ${range.basePort.toString()}\u2013${range.maxPort.toString()}`
|
|
2611
2967
|
);
|
|
2612
2968
|
}
|
|
2613
|
-
async function selectRegistrationCandidate(input, target, excluded) {
|
|
2969
|
+
async function selectRegistrationCandidate(input, target, excluded, range, scanOffset) {
|
|
2970
|
+
await readAndPruneActiveSessions(input.stateAccess);
|
|
2614
2971
|
return await withFileLock(stateLockPath(), async () => {
|
|
2615
|
-
const
|
|
2616
|
-
const
|
|
2972
|
+
const raw = await readStateRaw();
|
|
2973
|
+
const local = raw.sessions.filter((session) => session.hostname === getHostname2());
|
|
2974
|
+
const existing = local.find(
|
|
2617
2975
|
(session) => matchesRegistrationTarget(session, input, target)
|
|
2618
2976
|
);
|
|
2619
2977
|
if (existing !== void 0) {
|
|
2620
2978
|
return { existing };
|
|
2621
2979
|
}
|
|
2622
2980
|
const reserved = /* @__PURE__ */ new Set([
|
|
2623
|
-
...
|
|
2981
|
+
...local.map((session) => session.localPort),
|
|
2624
2982
|
...excluded
|
|
2625
2983
|
]);
|
|
2626
2984
|
return {
|
|
@@ -2628,20 +2986,58 @@ async function selectRegistrationCandidate(input, target, excluded) {
|
|
|
2628
2986
|
input.preferredPort,
|
|
2629
2987
|
reserved,
|
|
2630
2988
|
() => Promise.resolve(true),
|
|
2631
|
-
|
|
2632
|
-
|
|
2989
|
+
range,
|
|
2990
|
+
scanOffset
|
|
2633
2991
|
)
|
|
2634
2992
|
};
|
|
2635
2993
|
}, input.stateAccess);
|
|
2636
2994
|
}
|
|
2995
|
+
function validateTcpPort(value, label) {
|
|
2996
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > 65535) {
|
|
2997
|
+
throw new CfDebuggerError("UNSAFE_INPUT", `${label} must be from 1 to 65535.`);
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
function validatePortRange(input) {
|
|
3001
|
+
const basePort = input.basePort ?? DEFAULT_BASE_PORT;
|
|
3002
|
+
const maxPort = input.maxPort ?? DEFAULT_MAX_PORT;
|
|
3003
|
+
validateTcpPort(basePort, "Local port range start");
|
|
3004
|
+
validateTcpPort(maxPort, "Local port range end");
|
|
3005
|
+
if (basePort > maxPort) {
|
|
3006
|
+
throw new CfDebuggerError(
|
|
3007
|
+
"UNSAFE_INPUT",
|
|
3008
|
+
"Local port range start must not exceed its end."
|
|
3009
|
+
);
|
|
3010
|
+
}
|
|
3011
|
+
if (input.preferredPort !== void 0) {
|
|
3012
|
+
validateTcpPort(input.preferredPort, "Preferred local port");
|
|
3013
|
+
}
|
|
3014
|
+
return { basePort, maxPort };
|
|
3015
|
+
}
|
|
2637
3016
|
function validateSessionInput(input) {
|
|
3017
|
+
validateCfCliOperand(input.region, "Region");
|
|
3018
|
+
validateCfCliOperand(input.org, "Cloud Foundry org");
|
|
3019
|
+
validateCfCliOperand(input.space, "Cloud Foundry space");
|
|
3020
|
+
validateCfCliOperand(input.app, "Cloud Foundry app");
|
|
3021
|
+
validateCfCliOperand(input.apiEndpoint, "Cloud Foundry API endpoint");
|
|
2638
3022
|
const remotePort = input.remotePort ?? DEFAULT_NODE_INSPECTOR_PORT;
|
|
2639
|
-
|
|
2640
|
-
throw new CfDebuggerError("UNSAFE_INPUT", "Remote inspector port must be from 1 to 65535.");
|
|
2641
|
-
}
|
|
3023
|
+
validateTcpPort(remotePort, "Remote inspector port");
|
|
2642
3024
|
if (input.startupTimeoutMs !== void 0 && (!Number.isSafeInteger(input.startupTimeoutMs) || input.startupTimeoutMs <= 0 || input.startupTimeoutMs > MAX_STARTUP_TIMEOUT_MS)) {
|
|
2643
3025
|
throw new CfDebuggerError("UNSAFE_INPUT", "Persisted startup timeout is outside the supported range.");
|
|
2644
3026
|
}
|
|
3027
|
+
return validatePortRange(input);
|
|
3028
|
+
}
|
|
3029
|
+
function registrationScanOffset(input, target, range) {
|
|
3030
|
+
const identity = JSON.stringify([
|
|
3031
|
+
input.apiEndpoint,
|
|
3032
|
+
input.region,
|
|
3033
|
+
input.org,
|
|
3034
|
+
input.space,
|
|
3035
|
+
input.app,
|
|
3036
|
+
target.process,
|
|
3037
|
+
target.instance
|
|
3038
|
+
]);
|
|
3039
|
+
const hash = createHash("sha256").update(identity).digest().readUInt32BE(0);
|
|
3040
|
+
return hash % (range.maxPort - range.basePort + 1);
|
|
2645
3041
|
}
|
|
2646
3042
|
function createRegisteredSession(input, target, localPort, controllerProcessIdentity) {
|
|
2647
3043
|
const sessionId = (input.sessionIdFactory ?? randomUUID2)();
|
|
@@ -2654,8 +3050,8 @@ function createRegisteredSession(input, target, localPort, controllerProcessIden
|
|
|
2654
3050
|
}
|
|
2655
3051
|
return {
|
|
2656
3052
|
sessionId,
|
|
2657
|
-
pid:
|
|
2658
|
-
controllerPid:
|
|
3053
|
+
pid: nodeProcess6.pid,
|
|
3054
|
+
controllerPid: nodeProcess6.pid,
|
|
2659
3055
|
...controllerProcessIdentity === void 0 ? {} : { controllerProcessIdentity },
|
|
2660
3056
|
hostname: getHostname2(),
|
|
2661
3057
|
region: input.region,
|
|
@@ -2675,37 +3071,46 @@ function createRegisteredSession(input, target, localPort, controllerProcessIden
|
|
|
2675
3071
|
};
|
|
2676
3072
|
}
|
|
2677
3073
|
async function persistRegistration(input, target, candidate, controllerProcessIdentity) {
|
|
3074
|
+
await readAndPruneActiveSessions(input.stateAccess);
|
|
2678
3075
|
return await withFileLock(stateLockPath(), async () => {
|
|
2679
|
-
const
|
|
2680
|
-
const
|
|
3076
|
+
const raw = await readStateRaw();
|
|
3077
|
+
const local = raw.sessions.filter((session2) => session2.hostname === getHostname2());
|
|
3078
|
+
const existing = local.find(
|
|
2681
3079
|
(session2) => matchesRegistrationTarget(session2, input, target)
|
|
2682
3080
|
);
|
|
2683
3081
|
if (existing !== void 0) {
|
|
2684
3082
|
return { session: existing, existing };
|
|
2685
3083
|
}
|
|
2686
|
-
const reserved =
|
|
3084
|
+
const reserved = local.some((session2) => session2.localPort === candidate);
|
|
2687
3085
|
if (reserved || !await input.portProbe(candidate)) {
|
|
2688
3086
|
return void 0;
|
|
2689
3087
|
}
|
|
2690
3088
|
const session = createRegisteredSession(input, target, candidate, controllerProcessIdentity);
|
|
2691
3089
|
await writeState({
|
|
2692
3090
|
version: "2",
|
|
2693
|
-
sessions: [...
|
|
3091
|
+
sessions: [...raw.sessions, session]
|
|
2694
3092
|
});
|
|
2695
3093
|
return { session };
|
|
2696
3094
|
}, input.stateAccess);
|
|
2697
3095
|
}
|
|
2698
3096
|
async function registerNewSession(input) {
|
|
2699
|
-
validateSessionInput(input);
|
|
3097
|
+
const range = validateSessionInput(input);
|
|
2700
3098
|
const target = resolveNodeTarget(input);
|
|
3099
|
+
const scanOffset = registrationScanOffset(input, target, range);
|
|
2701
3100
|
const controllerIdentity = await readProcessIdentity(
|
|
2702
|
-
|
|
3101
|
+
nodeProcess6.pid,
|
|
2703
3102
|
input.stateAccess?.signal
|
|
2704
3103
|
);
|
|
2705
3104
|
const excluded = /* @__PURE__ */ new Set();
|
|
2706
|
-
const maximumAttempts =
|
|
3105
|
+
const maximumAttempts = range.maxPort - range.basePort + 2;
|
|
2707
3106
|
for (let attempt = 0; attempt < maximumAttempts; attempt += 1) {
|
|
2708
|
-
const selection = await selectRegistrationCandidate(
|
|
3107
|
+
const selection = await selectRegistrationCandidate(
|
|
3108
|
+
input,
|
|
3109
|
+
target,
|
|
3110
|
+
excluded,
|
|
3111
|
+
range,
|
|
3112
|
+
scanOffset
|
|
3113
|
+
);
|
|
2709
3114
|
if (selection.existing !== void 0) {
|
|
2710
3115
|
return { session: selection.existing, existing: selection.existing };
|
|
2711
3116
|
}
|
|
@@ -2819,9 +3224,6 @@ async function requestSessionStop(sessionId) {
|
|
|
2819
3224
|
if (target === void 0) {
|
|
2820
3225
|
return void 0;
|
|
2821
3226
|
}
|
|
2822
|
-
if (target.status === "ready") {
|
|
2823
|
-
return { session: target, previousStatus: target.status };
|
|
2824
|
-
}
|
|
2825
3227
|
await writeSessionStopIntent(sessionId);
|
|
2826
3228
|
if (target.stopRequestedAt !== void 0) {
|
|
2827
3229
|
return { session: target, previousStatus: target.status };
|
|
@@ -2875,51 +3277,59 @@ function cliErrorExitCode(error) {
|
|
|
2875
3277
|
}
|
|
2876
3278
|
return hasCleanupFailure(error) ? CLEANUP_FAILURE_EXIT_CODE : 1;
|
|
2877
3279
|
}
|
|
3280
|
+
function stopAllExitCode(errors) {
|
|
3281
|
+
if (errors.length === 0) {
|
|
3282
|
+
return void 0;
|
|
3283
|
+
}
|
|
3284
|
+
return errors.some((error) => hasTunnelTerminationFailure(error)) ? CLEANUP_FAILURE_EXIT_CODE : 1;
|
|
3285
|
+
}
|
|
2878
3286
|
|
|
2879
3287
|
// src/debug-session/lifecycle.ts
|
|
2880
3288
|
import { constants as osConstants } from "os";
|
|
2881
3289
|
|
|
2882
3290
|
// src/debug-session/processes.ts
|
|
2883
3291
|
import process3 from "process";
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
return "terminated";
|
|
2889
|
-
}
|
|
2890
|
-
if (verifyBeforeSignal !== void 0 && !await verifyBeforeSignal("SIGTERM")) {
|
|
2891
|
-
return "ownership-lost";
|
|
2892
|
-
}
|
|
3292
|
+
function terminationTargetAlive(pid, targetKind) {
|
|
3293
|
+
return targetKind === "group" ? isProcessGroupAlive(pid) : isPidAlive(pid);
|
|
3294
|
+
}
|
|
3295
|
+
function signalTerminationTarget(pid, targetKind, signal) {
|
|
2893
3296
|
try {
|
|
2894
|
-
process3.kill(targetKind === "group" ? -pid : pid,
|
|
3297
|
+
process3.kill(targetKind === "group" ? -pid : pid, signal);
|
|
2895
3298
|
} catch {
|
|
2896
3299
|
}
|
|
3300
|
+
}
|
|
3301
|
+
async function waitForTargetExit(pid, targetKind, timeoutMs) {
|
|
2897
3302
|
const startedAt = Date.now();
|
|
2898
3303
|
while (Date.now() - startedAt < timeoutMs) {
|
|
2899
|
-
if (!
|
|
2900
|
-
return
|
|
3304
|
+
if (!terminationTargetAlive(pid, targetKind)) {
|
|
3305
|
+
return true;
|
|
2901
3306
|
}
|
|
2902
3307
|
await new Promise((resolve) => {
|
|
2903
3308
|
setTimeout(resolve, PID_TERMINATION_POLL_MS);
|
|
2904
3309
|
});
|
|
2905
3310
|
}
|
|
2906
|
-
|
|
3311
|
+
return !terminationTargetAlive(pid, targetKind);
|
|
3312
|
+
}
|
|
3313
|
+
async function signalIsAuthorized(verifier, signal) {
|
|
3314
|
+
return verifier === void 0 || await verifier(signal);
|
|
3315
|
+
}
|
|
3316
|
+
async function terminatePidOrGroup(pid, timeoutMs = CHILD_SIGTERM_GRACE_MS, pinnedTarget, verifyBeforeSignal) {
|
|
3317
|
+
const targetKind = pinnedTarget ?? (isProcessGroupAlive(pid) ? "group" : "pid");
|
|
3318
|
+
if (!terminationTargetAlive(pid, targetKind)) {
|
|
3319
|
+
return "terminated";
|
|
3320
|
+
}
|
|
3321
|
+
if (!await signalIsAuthorized(verifyBeforeSignal, "SIGTERM")) {
|
|
2907
3322
|
return "ownership-lost";
|
|
2908
3323
|
}
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
3324
|
+
signalTerminationTarget(pid, targetKind, "SIGTERM");
|
|
3325
|
+
if (await waitForTargetExit(pid, targetKind, timeoutMs)) {
|
|
3326
|
+
return "terminated";
|
|
2912
3327
|
}
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
if (!targetAlive()) {
|
|
2916
|
-
return "terminated";
|
|
2917
|
-
}
|
|
2918
|
-
await new Promise((resolve) => {
|
|
2919
|
-
setTimeout(resolve, PID_TERMINATION_POLL_MS);
|
|
2920
|
-
});
|
|
3328
|
+
if (!await signalIsAuthorized(verifyBeforeSignal, "SIGKILL")) {
|
|
3329
|
+
return "ownership-lost";
|
|
2921
3330
|
}
|
|
2922
|
-
|
|
3331
|
+
signalTerminationTarget(pid, targetKind, "SIGKILL");
|
|
3332
|
+
return await waitForTargetExit(pid, targetKind, CHILD_SIGKILL_GRACE_MS) ? "terminated" : "still-alive";
|
|
2923
3333
|
}
|
|
2924
3334
|
async function killProcessGroupOrProc(child, verifyBeforeSignal, timeoutMs = CHILD_SIGTERM_GRACE_MS) {
|
|
2925
3335
|
if (child.pid === void 0) {
|
|
@@ -2938,7 +3348,39 @@ async function killProcessGroupOrProc(child, verifyBeforeSignal, timeoutMs = CHI
|
|
|
2938
3348
|
}
|
|
2939
3349
|
|
|
2940
3350
|
// src/debug-session/session-home.ts
|
|
2941
|
-
import { rm } from "fs/promises";
|
|
3351
|
+
import { lstat, rm } from "fs/promises";
|
|
3352
|
+
import { dirname as dirname3 } from "path";
|
|
3353
|
+
function errorCode7(error) {
|
|
3354
|
+
if (typeof error !== "object" || error === null) {
|
|
3355
|
+
return void 0;
|
|
3356
|
+
}
|
|
3357
|
+
const code = Reflect.get(error, "code");
|
|
3358
|
+
return typeof code === "string" ? code : void 0;
|
|
3359
|
+
}
|
|
3360
|
+
async function inspectSessionHomesRoot(root) {
|
|
3361
|
+
try {
|
|
3362
|
+
const stats = await lstat(root);
|
|
3363
|
+
if (stats.isSymbolicLink()) {
|
|
3364
|
+
return {
|
|
3365
|
+
status: "unsafe",
|
|
3366
|
+
reason: `Debugger CF homes root ${root} is a symbolic link; refusing to traverse it.`
|
|
3367
|
+
};
|
|
3368
|
+
}
|
|
3369
|
+
return stats.isDirectory() ? { status: "safe" } : {
|
|
3370
|
+
status: "unsafe",
|
|
3371
|
+
reason: `Debugger CF homes root ${root} is not a directory; refusing to traverse it.`
|
|
3372
|
+
};
|
|
3373
|
+
} catch (error) {
|
|
3374
|
+
if (errorCode7(error) === "ENOENT") {
|
|
3375
|
+
return { status: "absent" };
|
|
3376
|
+
}
|
|
3377
|
+
const code = errorCode7(error) ?? "unknown error";
|
|
3378
|
+
return {
|
|
3379
|
+
status: "unsafe",
|
|
3380
|
+
reason: `Debugger CF homes root ${root} could not be inspected (${code}); refusing to traverse it.`
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
2942
3384
|
async function removeOwnedSessionCfHome(sessionId, candidate) {
|
|
2943
3385
|
if (!isOwnedSessionCfHomeDir(sessionId, candidate)) {
|
|
2944
3386
|
throw new CfDebuggerError(
|
|
@@ -2946,6 +3388,10 @@ async function removeOwnedSessionCfHome(sessionId, candidate) {
|
|
|
2946
3388
|
`Refusing to remove unowned debugger CF home for session ${sessionId}.`
|
|
2947
3389
|
);
|
|
2948
3390
|
}
|
|
3391
|
+
const rootInspection = await inspectSessionHomesRoot(dirname3(candidate));
|
|
3392
|
+
if (rootInspection.status === "unsafe") {
|
|
3393
|
+
throw new CfDebuggerError("UNSAFE_INPUT", rootInspection.reason);
|
|
3394
|
+
}
|
|
2949
3395
|
await rm(candidate, { recursive: true, force: true });
|
|
2950
3396
|
}
|
|
2951
3397
|
async function tryRemoveOwnedSessionCfHome(sessionId, candidate) {
|
|
@@ -2960,13 +3406,30 @@ async function tryRemoveOwnedSessionCfHome(sessionId, candidate) {
|
|
|
2960
3406
|
}
|
|
2961
3407
|
}
|
|
2962
3408
|
|
|
3409
|
+
// src/debug-session/session-cleanup.ts
|
|
3410
|
+
async function forgetSessionThenCleanupHome(session) {
|
|
3411
|
+
const removed = await removeSession(session.sessionId);
|
|
3412
|
+
if (!isOwnedSessionCfHomeDir(session.sessionId, session.cfHomeDir)) {
|
|
3413
|
+
return { homeStatus: "unowned", removed };
|
|
3414
|
+
}
|
|
3415
|
+
const homeRemoved = await tryRemoveOwnedSessionCfHome(
|
|
3416
|
+
session.sessionId,
|
|
3417
|
+
session.cfHomeDir
|
|
3418
|
+
);
|
|
3419
|
+
return {
|
|
3420
|
+
homeStatus: homeRemoved ? "removed" : "retained",
|
|
3421
|
+
removed
|
|
3422
|
+
};
|
|
3423
|
+
}
|
|
3424
|
+
|
|
2963
3425
|
// src/debug-session/lifecycle.ts
|
|
2964
3426
|
var handleLifecycles = /* @__PURE__ */ new WeakMap();
|
|
2965
|
-
function
|
|
3427
|
+
function childIsOpen2(child) {
|
|
2966
3428
|
return child.exitCode === null && child.signalCode === null;
|
|
2967
3429
|
}
|
|
2968
3430
|
function signalExitCode(signal) {
|
|
2969
|
-
|
|
3431
|
+
const signum = osConstants.signals[signal];
|
|
3432
|
+
return typeof signum === "number" && Number.isFinite(signum) ? 128 + signum : 1;
|
|
2970
3433
|
}
|
|
2971
3434
|
function unexpectedExitCode(code, signal) {
|
|
2972
3435
|
if (signal !== null) {
|
|
@@ -2980,6 +3443,8 @@ function emitSafely(emit, status, message) {
|
|
|
2980
3443
|
} catch {
|
|
2981
3444
|
}
|
|
2982
3445
|
}
|
|
3446
|
+
function ignoreCleanupFailure() {
|
|
3447
|
+
}
|
|
2983
3448
|
async function runCleanupActions(actions, aggregateMessage) {
|
|
2984
3449
|
const errors = [];
|
|
2985
3450
|
for (const action of actions) {
|
|
@@ -2997,10 +3462,10 @@ async function runCleanupActions(actions, aggregateMessage) {
|
|
|
2997
3462
|
}
|
|
2998
3463
|
}
|
|
2999
3464
|
async function handleTunnelClose(sessionId, code, signal, child, resolveExit, isFinalizing, hasFailed, markFailed, emit) {
|
|
3000
|
-
const stopRequested = await
|
|
3465
|
+
const stopRequested = isFinalizing() ? false : await hasExpectedStopSignal(sessionId);
|
|
3001
3466
|
const expected = isFinalizing() || stopRequested;
|
|
3002
3467
|
if (stopRequested) {
|
|
3003
|
-
await clearSessionStopIntent(sessionId);
|
|
3468
|
+
await clearSessionStopIntent(sessionId).catch(ignoreCleanupFailure);
|
|
3004
3469
|
}
|
|
3005
3470
|
if (!expected && !hasFailed()) {
|
|
3006
3471
|
const detail = signal === null ? code === null ? "no exit status" : `exit code ${code.toString()}` : `signal ${signal}`;
|
|
@@ -3015,6 +3480,19 @@ async function handleTunnelClose(sessionId, code, signal, child, resolveExit, is
|
|
|
3015
3480
|
}
|
|
3016
3481
|
resolveExit(expected && !hasFailed() ? 0 : unexpectedExitCode(code, signal));
|
|
3017
3482
|
}
|
|
3483
|
+
async function hasExpectedStopSignal(sessionId) {
|
|
3484
|
+
let sidecarRequested = false;
|
|
3485
|
+
try {
|
|
3486
|
+
sidecarRequested = await hasSessionStopIntent(sessionId);
|
|
3487
|
+
} catch {
|
|
3488
|
+
}
|
|
3489
|
+
try {
|
|
3490
|
+
const verdict = await inspectSessionStateStopIntent(sessionId);
|
|
3491
|
+
return verdict === "requested" || verdict === "missing" && sidecarRequested;
|
|
3492
|
+
} catch {
|
|
3493
|
+
return false;
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3018
3496
|
function attachTunnelEvents(sessionId, child, resolveExit, isFinalizing, hasFailed, markFailed, emit) {
|
|
3019
3497
|
child.once("close", (code, signal) => {
|
|
3020
3498
|
void handleTunnelClose(
|
|
@@ -3057,87 +3535,126 @@ async function expectedTunnelStillOwned(session, child) {
|
|
|
3057
3535
|
return ownership.status === "unverified" ? "unverified" : ownership.status === "owned";
|
|
3058
3536
|
}
|
|
3059
3537
|
async function cleanupFinishedSession(session) {
|
|
3060
|
-
await
|
|
3061
|
-
await
|
|
3062
|
-
|
|
3538
|
+
const cleanup = await forgetSessionThenCleanupHome(session);
|
|
3539
|
+
await clearSessionStopIntent(session.sessionId).catch(ignoreCleanupFailure);
|
|
3540
|
+
if (cleanup.homeStatus !== "removed") {
|
|
3541
|
+
throw new CfDebuggerError(
|
|
3542
|
+
"CF_HOME_CLEANUP_FAILED",
|
|
3543
|
+
cleanup.homeStatus === "retained" ? `Session state was removed, but owned CF home ${session.cfHomeDir} could not be deleted. It may contain a live CF refresh token; remove it manually or run \`cf-debugger doctor --cleanup\`.` : `Session state was removed, but unowned CF home ${session.cfHomeDir} was not deleted.`
|
|
3544
|
+
);
|
|
3545
|
+
}
|
|
3063
3546
|
}
|
|
3064
|
-
function
|
|
3065
|
-
let child;
|
|
3066
|
-
let childIdentity;
|
|
3067
|
-
let childFailed = false;
|
|
3068
|
-
let tunnelError;
|
|
3069
|
-
let finalizing = false;
|
|
3547
|
+
function initializeTunnelLifecycle() {
|
|
3070
3548
|
let exitResolve = (_code) => {
|
|
3071
3549
|
throw new Error("Tunnel exit resolver was used before initialization.");
|
|
3072
3550
|
};
|
|
3073
3551
|
const exitPromise = new Promise((resolve) => {
|
|
3074
3552
|
exitResolve = resolve;
|
|
3075
3553
|
});
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
(error) => {
|
|
3086
|
-
if (!childFailed) {
|
|
3087
|
-
childFailed = true;
|
|
3088
|
-
tunnelError = error;
|
|
3089
|
-
}
|
|
3090
|
-
},
|
|
3091
|
-
emit
|
|
3092
|
-
);
|
|
3093
|
-
};
|
|
3094
|
-
const verifyBeforeSignal = async (signal) => {
|
|
3095
|
-
const tunnelChild = child;
|
|
3096
|
-
const childPid = tunnelChild?.pid;
|
|
3097
|
-
if (tunnelChild === void 0 || childPid === void 0 || !childIsOpen(tunnelChild)) {
|
|
3098
|
-
return false;
|
|
3099
|
-
}
|
|
3100
|
-
const expectedIdentity = await childIdentity;
|
|
3101
|
-
if (expectedIdentity !== void 0) {
|
|
3102
|
-
return await inspectProcessIdentity(childPid, expectedIdentity) === "match" && childIsOpen(tunnelChild);
|
|
3103
|
-
}
|
|
3104
|
-
if (signal === "SIGKILL") {
|
|
3105
|
-
return await expectedTunnelStillOwned(session, tunnelChild) === true;
|
|
3554
|
+
return {
|
|
3555
|
+
exitPromise,
|
|
3556
|
+
state: {
|
|
3557
|
+
child: void 0,
|
|
3558
|
+
childIdentity: void 0,
|
|
3559
|
+
childFailed: false,
|
|
3560
|
+
tunnelError: void 0,
|
|
3561
|
+
finalizing: false,
|
|
3562
|
+
exitResolve
|
|
3106
3563
|
}
|
|
3107
|
-
return true;
|
|
3108
3564
|
};
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3565
|
+
}
|
|
3566
|
+
function recordTunnelFailure(state, error) {
|
|
3567
|
+
if (!state.childFailed) {
|
|
3568
|
+
state.childFailed = true;
|
|
3569
|
+
state.tunnelError = error;
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
function observeTunnelChild(state, sessionId, tunnelChild, emit) {
|
|
3573
|
+
state.child = tunnelChild;
|
|
3574
|
+
state.childIdentity = tunnelChild.pid === void 0 ? void 0 : readProcessIdentity(tunnelChild.pid);
|
|
3575
|
+
attachTunnelEvents(
|
|
3576
|
+
sessionId,
|
|
3577
|
+
tunnelChild,
|
|
3578
|
+
state.exitResolve,
|
|
3579
|
+
() => state.finalizing,
|
|
3580
|
+
() => state.childFailed,
|
|
3581
|
+
(error) => {
|
|
3582
|
+
recordTunnelFailure(state, error);
|
|
3583
|
+
},
|
|
3584
|
+
emit
|
|
3585
|
+
);
|
|
3586
|
+
}
|
|
3587
|
+
async function verifyTunnelBeforeSignal(state, session, signal) {
|
|
3588
|
+
const tunnelChild = state.child;
|
|
3589
|
+
const childPid = tunnelChild?.pid;
|
|
3590
|
+
if (tunnelChild === void 0 || childPid === void 0 || !childIsOpen2(tunnelChild)) {
|
|
3591
|
+
return false;
|
|
3592
|
+
}
|
|
3593
|
+
const expectedIdentity = await state.childIdentity;
|
|
3594
|
+
if (expectedIdentity !== void 0) {
|
|
3595
|
+
return await inspectProcessIdentity(childPid, expectedIdentity) === "match" && childIsOpen2(tunnelChild);
|
|
3596
|
+
}
|
|
3597
|
+
if (signal === "SIGKILL") {
|
|
3598
|
+
return await expectedTunnelStillOwned(session, tunnelChild) === true;
|
|
3599
|
+
}
|
|
3600
|
+
return true;
|
|
3601
|
+
}
|
|
3602
|
+
function tunnelTerminationError(session, child) {
|
|
3603
|
+
const stderr = child === void 0 ? void 0 : formatTunnelDiagnostics(getTunnelDiagnostics(child));
|
|
3604
|
+
return new CfDebuggerError(
|
|
3605
|
+
"TUNNEL_TERMINATION_FAILED",
|
|
3606
|
+
`Tunnel for session ${session.sessionId} did not terminate cleanly; state and CF home were retained.`,
|
|
3607
|
+
stderr
|
|
3608
|
+
);
|
|
3609
|
+
}
|
|
3610
|
+
async function finalizeTunnelLifecycle(state, session, emit, emitStopped) {
|
|
3611
|
+
state.finalizing = true;
|
|
3612
|
+
const verify = async (signal) => {
|
|
3613
|
+
return await verifyTunnelBeforeSignal(state, session, signal);
|
|
3125
3614
|
};
|
|
3615
|
+
const termination = state.child === void 0 ? "terminated" : await killProcessGroupOrProc(state.child, verify);
|
|
3616
|
+
const stillOwned = await expectedTunnelStillOwned(session, state.child);
|
|
3617
|
+
if (termination !== "terminated" || stillOwned === true) {
|
|
3618
|
+
throw tunnelTerminationError(session, state.child);
|
|
3619
|
+
}
|
|
3620
|
+
if (stillOwned === "unverified" && emitStopped) {
|
|
3621
|
+
emitSafely(
|
|
3622
|
+
emit,
|
|
3623
|
+
"stopping",
|
|
3624
|
+
"Tunnel process termination was confirmed, but local port ownership could not be rechecked. On macOS, install lsof to restore that diagnostic."
|
|
3625
|
+
);
|
|
3626
|
+
}
|
|
3627
|
+
await cleanupFinishedSession(session);
|
|
3628
|
+
if (emitStopped) {
|
|
3629
|
+
emitSafely(emit, "stopped");
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
function assertTunnelRunning(state, session) {
|
|
3633
|
+
if (state.child !== void 0 && childIsOpen2(state.child) && !state.childFailed) {
|
|
3634
|
+
return;
|
|
3635
|
+
}
|
|
3636
|
+
throw new CfDebuggerError(
|
|
3637
|
+
"TUNNEL_NOT_READY",
|
|
3638
|
+
`SSH tunnel for session ${session.sessionId} exited before readiness could be committed.`,
|
|
3639
|
+
state.child === void 0 ? void 0 : formatTunnelDiagnostics(getTunnelDiagnostics(state.child))
|
|
3640
|
+
);
|
|
3641
|
+
}
|
|
3642
|
+
function createTunnelLifecycle(session, emit) {
|
|
3643
|
+
const initialized = initializeTunnelLifecycle();
|
|
3644
|
+
const { state } = initialized;
|
|
3126
3645
|
return {
|
|
3127
|
-
exitPromise,
|
|
3646
|
+
exitPromise: initialized.exitPromise,
|
|
3128
3647
|
assertRunning: () => {
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3648
|
+
assertTunnelRunning(state, session);
|
|
3649
|
+
},
|
|
3650
|
+
finalize: async (emitStopped) => {
|
|
3651
|
+
await finalizeTunnelLifecycle(state, session, emit, emitStopped);
|
|
3652
|
+
},
|
|
3653
|
+
observeChild: (child) => {
|
|
3654
|
+
observeTunnelChild(state, session.sessionId, child, emit);
|
|
3136
3655
|
},
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
failed: () => childFailed,
|
|
3140
|
-
error: () => tunnelError
|
|
3656
|
+
failed: () => state.childFailed,
|
|
3657
|
+
error: () => state.tunnelError
|
|
3141
3658
|
};
|
|
3142
3659
|
}
|
|
3143
3660
|
function createDebuggerHandle(session, emit, lifecycle) {
|
|
@@ -3245,7 +3762,7 @@ function throwIfStartupAborted(signal, expiresAt, timeoutMs, phase) {
|
|
|
3245
3762
|
|
|
3246
3763
|
// src/debug-session/start.ts
|
|
3247
3764
|
import { chmod as chmod4, mkdir as mkdir4 } from "fs/promises";
|
|
3248
|
-
import
|
|
3765
|
+
import nodeProcess7 from "process";
|
|
3249
3766
|
|
|
3250
3767
|
// src/debug-session/orphans.ts
|
|
3251
3768
|
import { readdir as readdir2, stat as stat2, unlink as unlink4 } from "fs/promises";
|
|
@@ -3288,6 +3805,16 @@ async function pruneAndCleanupOrphans(stateAccess) {
|
|
|
3288
3805
|
|
|
3289
3806
|
// src/debug-session/startup-cancellation.ts
|
|
3290
3807
|
var STOP_REQUEST_POLL_MS = 500;
|
|
3808
|
+
async function startupStopWasRequested(sessionId) {
|
|
3809
|
+
try {
|
|
3810
|
+
if (await hasSessionStopIntent(sessionId)) {
|
|
3811
|
+
return true;
|
|
3812
|
+
}
|
|
3813
|
+
} catch {
|
|
3814
|
+
}
|
|
3815
|
+
const stateVerdict = await inspectSessionStateStopIntent(sessionId);
|
|
3816
|
+
return stateVerdict === "missing" || stateVerdict === "requested";
|
|
3817
|
+
}
|
|
3291
3818
|
function createStartupCancellation(sessionId, callerSignal) {
|
|
3292
3819
|
const controller = new AbortController();
|
|
3293
3820
|
let active = true;
|
|
@@ -3303,7 +3830,7 @@ function createStartupCancellation(sessionId, callerSignal) {
|
|
|
3303
3830
|
};
|
|
3304
3831
|
const poll = async () => {
|
|
3305
3832
|
try {
|
|
3306
|
-
if (active && await
|
|
3833
|
+
if (active && await startupStopWasRequested(sessionId)) {
|
|
3307
3834
|
controller.abort();
|
|
3308
3835
|
}
|
|
3309
3836
|
} catch {
|
|
@@ -3487,6 +4014,8 @@ async function signalRemoteNode(inputs) {
|
|
|
3487
4014
|
}
|
|
3488
4015
|
|
|
3489
4016
|
// src/debug-session/startup-tunnel.ts
|
|
4017
|
+
var INSPECTOR_READY_RESERVE_MS = 1e4;
|
|
4018
|
+
var OWNER_READY_ATTEMPT_MAX_MS = 5e3;
|
|
3490
4019
|
function linkAbortSignals(signals) {
|
|
3491
4020
|
const controller = new AbortController();
|
|
3492
4021
|
const subscriptions = [];
|
|
@@ -3510,6 +4039,35 @@ function linkAbortSignals(signals) {
|
|
|
3510
4039
|
}
|
|
3511
4040
|
};
|
|
3512
4041
|
}
|
|
4042
|
+
function observeChildExit(child) {
|
|
4043
|
+
let listening = true;
|
|
4044
|
+
let resolveExit;
|
|
4045
|
+
const onClose = () => {
|
|
4046
|
+
listening = false;
|
|
4047
|
+
resolveExit?.();
|
|
4048
|
+
};
|
|
4049
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
4050
|
+
return {
|
|
4051
|
+
promise: Promise.resolve(),
|
|
4052
|
+
dispose: () => {
|
|
4053
|
+
child.removeListener("close", onClose);
|
|
4054
|
+
}
|
|
4055
|
+
};
|
|
4056
|
+
}
|
|
4057
|
+
const promise = new Promise((resolve) => {
|
|
4058
|
+
resolveExit = resolve;
|
|
4059
|
+
child.once("close", onClose);
|
|
4060
|
+
});
|
|
4061
|
+
return {
|
|
4062
|
+
promise,
|
|
4063
|
+
dispose: () => {
|
|
4064
|
+
if (listening) {
|
|
4065
|
+
listening = false;
|
|
4066
|
+
child.removeListener("close", onClose);
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
};
|
|
4070
|
+
}
|
|
3513
4071
|
function diagnosticsStderr(child) {
|
|
3514
4072
|
return formatTunnelDiagnostics(getTunnelDiagnostics(child));
|
|
3515
4073
|
}
|
|
@@ -3530,6 +4088,33 @@ function remainingReadyTimeout(inputs) {
|
|
|
3530
4088
|
Math.min(inputs.tunnelReadyTimeoutMs, inputs.context.deadlineAt - Date.now())
|
|
3531
4089
|
);
|
|
3532
4090
|
}
|
|
4091
|
+
function createReadyWindow(timeoutMs) {
|
|
4092
|
+
const totalMs = Math.max(1, timeoutMs);
|
|
4093
|
+
const inspectorReserveMs = Math.min(
|
|
4094
|
+
INSPECTOR_READY_RESERVE_MS,
|
|
4095
|
+
Math.floor(totalMs / 2)
|
|
4096
|
+
);
|
|
4097
|
+
const ownerReserveMs = Math.min(
|
|
4098
|
+
OWNER_READY_ATTEMPT_MAX_MS * 2,
|
|
4099
|
+
Math.floor((totalMs - inspectorReserveMs) / 2)
|
|
4100
|
+
);
|
|
4101
|
+
const ownerTimeoutMs = Math.max(1, Math.floor(ownerReserveMs / 2));
|
|
4102
|
+
return {
|
|
4103
|
+
deadlineAt: Date.now() + totalMs,
|
|
4104
|
+
finalOwnerReserveMs: ownerTimeoutMs,
|
|
4105
|
+
localTimeoutMs: Math.max(1, totalMs - inspectorReserveMs - ownerReserveMs),
|
|
4106
|
+
ownerTimeoutMs
|
|
4107
|
+
};
|
|
4108
|
+
}
|
|
4109
|
+
function remainingWindowMs(window) {
|
|
4110
|
+
return Math.max(0, window.deadlineAt - Date.now());
|
|
4111
|
+
}
|
|
4112
|
+
function remainingInspectorMs(window) {
|
|
4113
|
+
return Math.max(
|
|
4114
|
+
1,
|
|
4115
|
+
remainingWindowMs(window) - window.finalOwnerReserveMs
|
|
4116
|
+
);
|
|
4117
|
+
}
|
|
3533
4118
|
function startupStateAccess(inputs) {
|
|
3534
4119
|
return {
|
|
3535
4120
|
...inputs.context.signal === void 0 ? {} : { signal: inputs.context.signal },
|
|
@@ -3554,6 +4139,7 @@ function ownerVerificationError(localPort, inspection, child) {
|
|
|
3554
4139
|
}
|
|
3555
4140
|
async function waitForLocalTunnel(inputs, child, timeoutMs) {
|
|
3556
4141
|
const childEnded = new AbortController();
|
|
4142
|
+
const childExit = observeChildExit(child);
|
|
3557
4143
|
const linked = linkAbortSignals([
|
|
3558
4144
|
...inputs.context.signal === void 0 ? [] : [inputs.context.signal],
|
|
3559
4145
|
childEnded.signal
|
|
@@ -3562,7 +4148,7 @@ async function waitForLocalTunnel(inputs, child, timeoutMs) {
|
|
|
3562
4148
|
const probe = probeTunnelReady(inputs.session.localPort, timeoutMs, linked.signal);
|
|
3563
4149
|
const winner = await Promise.race([
|
|
3564
4150
|
probe.then((ready) => ({ kind: "probe", ready })),
|
|
3565
|
-
|
|
4151
|
+
childExit.promise.then(() => ({ kind: "child-exit" }))
|
|
3566
4152
|
]);
|
|
3567
4153
|
if (winner.kind === "probe" && winner.ready) {
|
|
3568
4154
|
return;
|
|
@@ -3571,37 +4157,48 @@ async function waitForLocalTunnel(inputs, child, timeoutMs) {
|
|
|
3571
4157
|
childEnded.abort();
|
|
3572
4158
|
await probe.catch(() => false);
|
|
3573
4159
|
}
|
|
4160
|
+
} finally {
|
|
4161
|
+
childExit.dispose();
|
|
4162
|
+
linked.dispose();
|
|
4163
|
+
}
|
|
4164
|
+
throw new CfDebuggerError(
|
|
4165
|
+
"TUNNEL_NOT_READY",
|
|
4166
|
+
`SSH tunnel on local port ${inputs.session.localPort.toString()} did not bind within ${Math.round(timeoutMs / 1e3).toString()}s.`,
|
|
4167
|
+
diagnosticsStderr(child)
|
|
4168
|
+
);
|
|
4169
|
+
}
|
|
4170
|
+
async function verifyLocalOwner(inputs, child, childPid, timeoutMs) {
|
|
4171
|
+
const phaseTimeout = AbortSignal.timeout(Math.max(1, Math.floor(timeoutMs)));
|
|
4172
|
+
const linked = linkAbortSignals([
|
|
4173
|
+
...inputs.context.signal === void 0 ? [] : [inputs.context.signal],
|
|
4174
|
+
phaseTimeout
|
|
4175
|
+
]);
|
|
4176
|
+
let ownership;
|
|
4177
|
+
try {
|
|
4178
|
+
ownership = await inspectPortOwnership(
|
|
4179
|
+
inputs.session.localPort,
|
|
4180
|
+
childPid,
|
|
4181
|
+
linked.signal
|
|
4182
|
+
);
|
|
4183
|
+
} catch (error) {
|
|
4184
|
+
if (phaseTimeout.aborted && inputs.context.signal?.aborted !== true) {
|
|
4185
|
+
ownership = {
|
|
4186
|
+
status: "unverified",
|
|
4187
|
+
reason: `ownership inspection exceeded its ${Math.round(timeoutMs).toString()}ms readiness budget`
|
|
4188
|
+
};
|
|
4189
|
+
} else {
|
|
4190
|
+
throw error;
|
|
4191
|
+
}
|
|
3574
4192
|
} finally {
|
|
3575
4193
|
linked.dispose();
|
|
3576
4194
|
}
|
|
3577
|
-
throw new CfDebuggerError(
|
|
3578
|
-
"TUNNEL_NOT_READY",
|
|
3579
|
-
`SSH tunnel on local port ${inputs.session.localPort.toString()} did not bind within ${Math.round(timeoutMs / 1e3).toString()}s.`,
|
|
3580
|
-
diagnosticsStderr(child)
|
|
3581
|
-
);
|
|
3582
|
-
}
|
|
3583
|
-
async function verifyLocalOwner(inputs, child, childPid) {
|
|
3584
|
-
const ownership = await inspectPortOwnership(
|
|
3585
|
-
inputs.session.localPort,
|
|
3586
|
-
childPid,
|
|
3587
|
-
inputs.context.signal
|
|
3588
|
-
);
|
|
3589
4195
|
if (ownership.status !== "owned") {
|
|
3590
4196
|
throw ownerVerificationError(inputs.session.localPort, ownership, child);
|
|
3591
4197
|
}
|
|
3592
4198
|
}
|
|
3593
|
-
function childExitPromise(child) {
|
|
3594
|
-
if (child.exitCode !== null || child.signalCode !== null) {
|
|
3595
|
-
return Promise.resolve();
|
|
3596
|
-
}
|
|
3597
|
-
return new Promise((resolve) => {
|
|
3598
|
-
child.once("close", () => {
|
|
3599
|
-
resolve();
|
|
3600
|
-
});
|
|
3601
|
-
});
|
|
3602
|
-
}
|
|
3603
4199
|
async function verifyInspector(inputs, child, timeoutMs) {
|
|
3604
4200
|
const childEnded = new AbortController();
|
|
4201
|
+
const childExit = observeChildExit(child);
|
|
3605
4202
|
const linked = linkAbortSignals([
|
|
3606
4203
|
...inputs.context.signal === void 0 ? [] : [inputs.context.signal],
|
|
3607
4204
|
childEnded.signal
|
|
@@ -3610,7 +4207,7 @@ async function verifyInspector(inputs, child, timeoutMs) {
|
|
|
3610
4207
|
const probe = probeInspectorReady(inputs.session.localPort, timeoutMs, linked.signal);
|
|
3611
4208
|
const winner = await Promise.race([
|
|
3612
4209
|
probe.then((result) => ({ kind: "probe", result })),
|
|
3613
|
-
|
|
4210
|
+
childExit.promise.then(() => ({ kind: "child-exit" }))
|
|
3614
4211
|
]);
|
|
3615
4212
|
if (winner.kind === "probe" && winner.result.status === "ready") {
|
|
3616
4213
|
return;
|
|
@@ -3620,6 +4217,7 @@ async function verifyInspector(inputs, child, timeoutMs) {
|
|
|
3620
4217
|
await probe.catch(() => ({ status: "unreachable" }));
|
|
3621
4218
|
}
|
|
3622
4219
|
} finally {
|
|
4220
|
+
childExit.dispose();
|
|
3623
4221
|
linked.dispose();
|
|
3624
4222
|
}
|
|
3625
4223
|
throw new CfDebuggerError(
|
|
@@ -3677,15 +4275,14 @@ async function openReadyTunnel(inputs) {
|
|
|
3677
4275
|
inputs.onChild(child);
|
|
3678
4276
|
const childPid = requireChildPid(child);
|
|
3679
4277
|
await recordTunnelPid(inputs, childPid);
|
|
3680
|
-
const
|
|
3681
|
-
|
|
3682
|
-
await waitForLocalTunnel(inputs, child, timeoutMs);
|
|
4278
|
+
const readyWindow = createReadyWindow(remainingReadyTimeout(inputs));
|
|
4279
|
+
await waitForLocalTunnel(inputs, child, readyWindow.localTimeoutMs);
|
|
3683
4280
|
ensureStartupActive(inputs, "local tunnel binding");
|
|
3684
|
-
await verifyLocalOwner(inputs, child, childPid);
|
|
4281
|
+
await verifyLocalOwner(inputs, child, childPid, readyWindow.ownerTimeoutMs);
|
|
3685
4282
|
ensureStartupActive(inputs, "local tunnel ownership verification");
|
|
3686
|
-
await verifyInspector(inputs, child,
|
|
4283
|
+
await verifyInspector(inputs, child, remainingInspectorMs(readyWindow));
|
|
3687
4284
|
ensureStartupActive(inputs, "inspector readiness verification");
|
|
3688
|
-
await verifyLocalOwner(inputs, child, childPid);
|
|
4285
|
+
await verifyLocalOwner(inputs, child, childPid, remainingWindowMs(readyWindow));
|
|
3689
4286
|
ensureStartupActive(inputs, "final local tunnel ownership verification");
|
|
3690
4287
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
3691
4288
|
throw new CfDebuggerError(
|
|
@@ -3697,9 +4294,17 @@ async function openReadyTunnel(inputs) {
|
|
|
3697
4294
|
}
|
|
3698
4295
|
|
|
3699
4296
|
// src/debug-session/start.ts
|
|
4297
|
+
function validateStartTarget(options) {
|
|
4298
|
+
return {
|
|
4299
|
+
...options,
|
|
4300
|
+
app: validateCfCliOperand(options.app, "app"),
|
|
4301
|
+
org: validateCfCliOperand(options.org, "org"),
|
|
4302
|
+
space: validateCfCliOperand(options.space, "space")
|
|
4303
|
+
};
|
|
4304
|
+
}
|
|
3700
4305
|
function requireCredentials(options) {
|
|
3701
|
-
const email = options.email ??
|
|
3702
|
-
const password = options.password ??
|
|
4306
|
+
const email = options.email ?? nodeProcess7.env["SAP_EMAIL"];
|
|
4307
|
+
const password = options.password ?? nodeProcess7.env["SAP_PASSWORD"];
|
|
3703
4308
|
if (email === void 0 || email.length === 0) {
|
|
3704
4309
|
throw new CfDebuggerError(
|
|
3705
4310
|
"MISSING_CREDENTIALS",
|
|
@@ -3885,11 +4490,11 @@ async function establishDebuggerSession(inputs) {
|
|
|
3885
4490
|
return readySession;
|
|
3886
4491
|
}
|
|
3887
4492
|
function writeWarning(message) {
|
|
3888
|
-
|
|
4493
|
+
nodeProcess7.stderr.write(`[cf-debugger] warning: ${message}
|
|
3889
4494
|
`);
|
|
3890
4495
|
}
|
|
3891
4496
|
function writeTunnelOutput(stream, text) {
|
|
3892
|
-
|
|
4497
|
+
nodeProcess7.stderr.write(`[cf-debugger tunnel ${stream}] ${text}
|
|
3893
4498
|
`);
|
|
3894
4499
|
}
|
|
3895
4500
|
function retryMessage(status) {
|
|
@@ -3931,59 +4536,102 @@ function normalizeStartupError(error, expiresAt, timeoutMs) {
|
|
|
3931
4536
|
}
|
|
3932
4537
|
return error;
|
|
3933
4538
|
}
|
|
4539
|
+
function resolveStartupPlan(options) {
|
|
4540
|
+
const validatedOptions = validateStartTarget(options);
|
|
4541
|
+
const restartAllowed = applyRestartEnvironmentVeto(
|
|
4542
|
+
validatedOptions.allowSshEnableRestart,
|
|
4543
|
+
nodeProcess7.env["CF_DEBUGGER_ALLOW_RESTART"]
|
|
4544
|
+
);
|
|
4545
|
+
const effectiveOptions = restartAllowed === validatedOptions.allowSshEnableRestart ? validatedOptions : { ...validatedOptions, allowSshEnableRestart: restartAllowed };
|
|
4546
|
+
const target = resolveNodeTarget(effectiveOptions);
|
|
4547
|
+
const credentials = requireCredentials(effectiveOptions);
|
|
4548
|
+
const apiEndpoint = resolveApiEndpoint(
|
|
4549
|
+
effectiveOptions.region,
|
|
4550
|
+
effectiveOptions.apiEndpoint,
|
|
4551
|
+
writeWarning
|
|
4552
|
+
);
|
|
4553
|
+
const tunnelReadyTimeoutMs = resolveTunnelReadyTimeoutMs(
|
|
4554
|
+
effectiveOptions.tunnelReadyTimeoutMs
|
|
4555
|
+
);
|
|
4556
|
+
return {
|
|
4557
|
+
apiEndpoint,
|
|
4558
|
+
credentials,
|
|
4559
|
+
options: effectiveOptions,
|
|
4560
|
+
target,
|
|
4561
|
+
tracker: createStatusTracker(effectiveOptions),
|
|
4562
|
+
tunnelReadyTimeoutMs
|
|
4563
|
+
};
|
|
4564
|
+
}
|
|
4565
|
+
async function completeStartup(plan, deadline, resources) {
|
|
4566
|
+
throwIfStartupAborted(
|
|
4567
|
+
deadline.signal,
|
|
4568
|
+
deadline.expiresAt,
|
|
4569
|
+
deadline.timeoutMs,
|
|
4570
|
+
"initialization"
|
|
4571
|
+
);
|
|
4572
|
+
await pruneAndCleanupOrphans(startupStateAccess2(deadline.signal, deadline.expiresAt));
|
|
4573
|
+
throwIfStartupAborted(
|
|
4574
|
+
deadline.signal,
|
|
4575
|
+
deadline.expiresAt,
|
|
4576
|
+
deadline.timeoutMs,
|
|
4577
|
+
"state cleanup"
|
|
4578
|
+
);
|
|
4579
|
+
const session = await registerSession(
|
|
4580
|
+
plan.options,
|
|
4581
|
+
plan.target,
|
|
4582
|
+
plan.apiEndpoint,
|
|
4583
|
+
deadline
|
|
4584
|
+
);
|
|
4585
|
+
resources.cancellation = createStartupCancellation(session.sessionId, deadline.signal);
|
|
4586
|
+
const context = createCfContext(
|
|
4587
|
+
session,
|
|
4588
|
+
plan.credentials,
|
|
4589
|
+
resources.cancellation,
|
|
4590
|
+
deadline,
|
|
4591
|
+
plan.tracker,
|
|
4592
|
+
plan.options.verbose === true
|
|
4593
|
+
);
|
|
4594
|
+
resources.lifecycle = createTunnelLifecycle(session, plan.tracker.emit);
|
|
4595
|
+
const activeSession = await establishDebuggerSession({
|
|
4596
|
+
options: { ...plan.options, remotePort: session.remotePort },
|
|
4597
|
+
target: plan.target,
|
|
4598
|
+
session,
|
|
4599
|
+
context,
|
|
4600
|
+
credentials: plan.credentials,
|
|
4601
|
+
tunnelReadyTimeoutMs: plan.tunnelReadyTimeoutMs,
|
|
4602
|
+
emit: plan.tracker.emit,
|
|
4603
|
+
transition: createTransition(session.sessionId, context),
|
|
4604
|
+
lifecycle: resources.lifecycle
|
|
4605
|
+
});
|
|
4606
|
+
resources.cancellation.dispose();
|
|
4607
|
+
deadline.dispose();
|
|
4608
|
+
return createDebuggerHandle(activeSession, plan.tracker.emit, resources.lifecycle);
|
|
4609
|
+
}
|
|
4610
|
+
async function handleStartupFailure(error, plan, resources, deadline) {
|
|
4611
|
+
resources.cancellation?.dispose();
|
|
4612
|
+
deadline.dispose();
|
|
4613
|
+
const normalized = normalizeStartupError(
|
|
4614
|
+
error,
|
|
4615
|
+
deadline.expiresAt,
|
|
4616
|
+
deadline.timeoutMs
|
|
4617
|
+
);
|
|
4618
|
+
if (resources.lifecycle !== void 0) {
|
|
4619
|
+
return await cleanupFailedStartup(normalized, resources.lifecycle, plan.tracker.emit);
|
|
4620
|
+
}
|
|
4621
|
+
plan.tracker.emit(
|
|
4622
|
+
"error",
|
|
4623
|
+
normalized instanceof Error ? normalized.message : String(normalized)
|
|
4624
|
+
);
|
|
4625
|
+
throw normalized;
|
|
4626
|
+
}
|
|
3934
4627
|
async function startDebuggerUsingDeadline(options, deadline) {
|
|
3935
|
-
const
|
|
3936
|
-
const
|
|
3937
|
-
|
|
3938
|
-
const startupTimeoutMs = deadline.timeoutMs;
|
|
3939
|
-
const tunnelReadyTimeoutMs = resolveTunnelReadyTimeoutMs(options.tunnelReadyTimeoutMs);
|
|
3940
|
-
const tracker = createStatusTracker(options);
|
|
3941
|
-
tracker.emit("starting");
|
|
3942
|
-
let cancellation;
|
|
3943
|
-
let lifecycle;
|
|
4628
|
+
const plan = resolveStartupPlan(options);
|
|
4629
|
+
const resources = {};
|
|
4630
|
+
plan.tracker.emit("starting");
|
|
3944
4631
|
try {
|
|
3945
|
-
|
|
3946
|
-
await pruneAndCleanupOrphans(
|
|
3947
|
-
startupStateAccess2(deadline.signal, deadline.expiresAt)
|
|
3948
|
-
);
|
|
3949
|
-
throwIfStartupAborted(deadline.signal, deadline.expiresAt, startupTimeoutMs, "state cleanup");
|
|
3950
|
-
const session = await registerSession(options, target, apiEndpoint, deadline);
|
|
3951
|
-
cancellation = createStartupCancellation(session.sessionId, deadline.signal);
|
|
3952
|
-
const context = createCfContext(
|
|
3953
|
-
session,
|
|
3954
|
-
credentials,
|
|
3955
|
-
cancellation,
|
|
3956
|
-
deadline,
|
|
3957
|
-
tracker,
|
|
3958
|
-
options.verbose === true
|
|
3959
|
-
);
|
|
3960
|
-
lifecycle = createTunnelLifecycle(session, tracker.emit);
|
|
3961
|
-
const activeSession = await establishDebuggerSession({
|
|
3962
|
-
options: { ...options, remotePort: session.remotePort },
|
|
3963
|
-
target,
|
|
3964
|
-
session,
|
|
3965
|
-
context,
|
|
3966
|
-
credentials,
|
|
3967
|
-
tunnelReadyTimeoutMs,
|
|
3968
|
-
emit: tracker.emit,
|
|
3969
|
-
transition: createTransition(session.sessionId, context),
|
|
3970
|
-
lifecycle
|
|
3971
|
-
});
|
|
3972
|
-
cancellation.dispose();
|
|
3973
|
-
deadline.dispose();
|
|
3974
|
-
return createDebuggerHandle(activeSession, tracker.emit, lifecycle);
|
|
4632
|
+
return await completeStartup(plan, deadline, resources);
|
|
3975
4633
|
} catch (error) {
|
|
3976
|
-
|
|
3977
|
-
deadline.dispose();
|
|
3978
|
-
const normalized = normalizeStartupError(error, deadline.expiresAt, startupTimeoutMs);
|
|
3979
|
-
if (lifecycle === void 0) {
|
|
3980
|
-
tracker.emit(
|
|
3981
|
-
"error",
|
|
3982
|
-
normalized instanceof Error ? normalized.message : String(normalized)
|
|
3983
|
-
);
|
|
3984
|
-
throw normalized;
|
|
3985
|
-
}
|
|
3986
|
-
return await cleanupFailedStartup(normalized, lifecycle, tracker.emit);
|
|
4634
|
+
return await handleStartupFailure(error, plan, resources, deadline);
|
|
3987
4635
|
}
|
|
3988
4636
|
}
|
|
3989
4637
|
async function startDebuggerWithinDeadline(options, deadline) {
|
|
@@ -4002,31 +4650,261 @@ async function startDebugger(options) {
|
|
|
4002
4650
|
|
|
4003
4651
|
// src/debug-session/doctor.ts
|
|
4004
4652
|
import {
|
|
4005
|
-
lstat,
|
|
4006
|
-
readFile as
|
|
4007
|
-
readdir as
|
|
4653
|
+
lstat as lstat3,
|
|
4654
|
+
readFile as readFile7,
|
|
4655
|
+
readdir as readdir5,
|
|
4008
4656
|
unlink as unlink5
|
|
4009
4657
|
} from "fs/promises";
|
|
4010
|
-
import { hostname as
|
|
4658
|
+
import { hostname as hostname4 } from "os";
|
|
4659
|
+
import { join as join5 } from "path";
|
|
4660
|
+
import nodeProcess10 from "process";
|
|
4661
|
+
|
|
4662
|
+
// src/debug-session/doctor-homes.ts
|
|
4663
|
+
import { readdir as readdir3 } from "fs/promises";
|
|
4011
4664
|
import { join as join3 } from "path";
|
|
4012
|
-
|
|
4665
|
+
function entryReason(symbolicLink, directory, cleanupEligible) {
|
|
4666
|
+
if (symbolicLink) {
|
|
4667
|
+
return "entry is a symbolic link, not a debugger CF home directory";
|
|
4668
|
+
}
|
|
4669
|
+
if (!directory) {
|
|
4670
|
+
return "entry is not a directory";
|
|
4671
|
+
}
|
|
4672
|
+
return cleanupEligible ? void 0 : "entry name is not a safe debugger session ID";
|
|
4673
|
+
}
|
|
4674
|
+
async function discoverOrphanHomes(sessions) {
|
|
4675
|
+
const root = join3(saptoolsDir(), CF_DEBUGGER_HOMES_DIRNAME);
|
|
4676
|
+
const rootInspection = await inspectSessionHomesRoot(root);
|
|
4677
|
+
if (rootInspection.status === "unsafe") {
|
|
4678
|
+
return { candidates: [], warnings: [rootInspection.reason] };
|
|
4679
|
+
}
|
|
4680
|
+
if (rootInspection.status === "absent") {
|
|
4681
|
+
return { candidates: [], warnings: [] };
|
|
4682
|
+
}
|
|
4683
|
+
const claimed = new Set(sessions.map((session) => session.sessionId));
|
|
4684
|
+
const entries = await readdir3(root, { withFileTypes: true });
|
|
4685
|
+
const candidates = entries.filter((entry) => !entry.isDirectory() || !claimed.has(entry.name)).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
|
|
4686
|
+
const path = join3(root, entry.name);
|
|
4687
|
+
const cleanupEligible = entry.isDirectory() && isOwnedSessionCfHomeDir(entry.name, path);
|
|
4688
|
+
const reason = entryReason(
|
|
4689
|
+
entry.isSymbolicLink(),
|
|
4690
|
+
entry.isDirectory(),
|
|
4691
|
+
cleanupEligible
|
|
4692
|
+
);
|
|
4693
|
+
return {
|
|
4694
|
+
sessionId: entry.name,
|
|
4695
|
+
path,
|
|
4696
|
+
cleanupEligible,
|
|
4697
|
+
...reason === void 0 ? {} : { reason }
|
|
4698
|
+
};
|
|
4699
|
+
});
|
|
4700
|
+
return { candidates, warnings: [] };
|
|
4701
|
+
}
|
|
4702
|
+
|
|
4703
|
+
// src/debug-session/doctor-legacy.ts
|
|
4704
|
+
import { lstat as lstat2, readFile as readFile6, readdir as readdir4 } from "fs/promises";
|
|
4705
|
+
import { hostname as hostname2 } from "os";
|
|
4706
|
+
import { join as join4 } from "path";
|
|
4707
|
+
import nodeProcess8 from "process";
|
|
4708
|
+
var LEGACY_STATE_FILENAME = "cf-debugger-state.json";
|
|
4709
|
+
var LEGACY_LOCK_FILENAME = "cf-debugger-state.lock";
|
|
4710
|
+
var LEGACY_HOMES_DIRNAME = "cf-debugger-homes";
|
|
4711
|
+
function errorCode8(error) {
|
|
4712
|
+
if (typeof error !== "object" || error === null) {
|
|
4713
|
+
return void 0;
|
|
4714
|
+
}
|
|
4715
|
+
const code = Reflect.get(error, "code");
|
|
4716
|
+
return typeof code === "string" ? code : void 0;
|
|
4717
|
+
}
|
|
4718
|
+
function errorMessage(error) {
|
|
4719
|
+
return error instanceof Error ? error.message : String(error);
|
|
4720
|
+
}
|
|
4721
|
+
function field3(value, key) {
|
|
4722
|
+
return Reflect.get(value, key);
|
|
4723
|
+
}
|
|
4724
|
+
function optionalText(value, key) {
|
|
4725
|
+
const candidate = field3(value, key);
|
|
4726
|
+
return typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
|
|
4727
|
+
}
|
|
4728
|
+
function optionalInteger2(value, key, maximum = Number.MAX_SAFE_INTEGER) {
|
|
4729
|
+
const candidate = field3(value, key);
|
|
4730
|
+
return typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate > 0 && candidate <= maximum ? candidate : void 0;
|
|
4731
|
+
}
|
|
4732
|
+
function inspectLegacyPid(pid) {
|
|
4733
|
+
try {
|
|
4734
|
+
nodeProcess8.kill(pid, 0);
|
|
4735
|
+
return {
|
|
4736
|
+
liveness: "alive",
|
|
4737
|
+
reason: "PID exists, but v1 state cannot prove process identity or tunnel ownership"
|
|
4738
|
+
};
|
|
4739
|
+
} catch (error) {
|
|
4740
|
+
if (errorCode8(error) === "ESRCH") {
|
|
4741
|
+
return { liveness: "not-running" };
|
|
4742
|
+
}
|
|
4743
|
+
return {
|
|
4744
|
+
liveness: "unverified",
|
|
4745
|
+
reason: `PID liveness check failed: ${errorMessage(error)}`
|
|
4746
|
+
};
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
function parseLegacySession(value, index) {
|
|
4750
|
+
if (typeof value !== "object" || value === null) {
|
|
4751
|
+
return { index, liveness: "unverified", reason: "legacy session entry is not an object" };
|
|
4752
|
+
}
|
|
4753
|
+
const sessionId = optionalText(value, "sessionId");
|
|
4754
|
+
const pid = optionalInteger2(value, "pid");
|
|
4755
|
+
const localPort = optionalInteger2(value, "localPort", 65535);
|
|
4756
|
+
const ownerHostname = optionalText(value, "hostname");
|
|
4757
|
+
const fields = {
|
|
4758
|
+
index,
|
|
4759
|
+
...sessionId === void 0 ? {} : { sessionId },
|
|
4760
|
+
...pid === void 0 ? {} : { pid },
|
|
4761
|
+
...localPort === void 0 ? {} : { localPort },
|
|
4762
|
+
...ownerHostname === void 0 ? {} : { hostname: ownerHostname }
|
|
4763
|
+
};
|
|
4764
|
+
if (pid === void 0 || ownerHostname === void 0) {
|
|
4765
|
+
return {
|
|
4766
|
+
...fields,
|
|
4767
|
+
liveness: "unverified",
|
|
4768
|
+
reason: pid === void 0 ? "legacy session has no valid PID" : "legacy session has no valid hostname"
|
|
4769
|
+
};
|
|
4770
|
+
}
|
|
4771
|
+
if (ownerHostname !== hostname2()) {
|
|
4772
|
+
return {
|
|
4773
|
+
...fields,
|
|
4774
|
+
liveness: "unverified",
|
|
4775
|
+
reason: `legacy session belongs to host ${ownerHostname}; local PID was not probed`
|
|
4776
|
+
};
|
|
4777
|
+
}
|
|
4778
|
+
return { ...fields, ...inspectLegacyPid(pid) };
|
|
4779
|
+
}
|
|
4780
|
+
async function inspectLegacyState(path) {
|
|
4781
|
+
let raw;
|
|
4782
|
+
try {
|
|
4783
|
+
raw = await readFile6(path, "utf8");
|
|
4784
|
+
} catch (error) {
|
|
4785
|
+
return errorCode8(error) === "ENOENT" ? { present: false } : { present: true, warning: `Could not read legacy v1 state: ${errorMessage(error)}` };
|
|
4786
|
+
}
|
|
4787
|
+
let parsed;
|
|
4788
|
+
try {
|
|
4789
|
+
parsed = JSON.parse(raw);
|
|
4790
|
+
} catch {
|
|
4791
|
+
return { present: true, warning: "Legacy v1 state contains invalid JSON." };
|
|
4792
|
+
}
|
|
4793
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
4794
|
+
return { present: true, warning: "Legacy v1 state is not an object." };
|
|
4795
|
+
}
|
|
4796
|
+
const sessions = field3(parsed, "sessions");
|
|
4797
|
+
if (field3(parsed, "version") !== "1" || !Array.isArray(sessions)) {
|
|
4798
|
+
return { present: true, warning: "Legacy v1 state has an invalid structure." };
|
|
4799
|
+
}
|
|
4800
|
+
return {
|
|
4801
|
+
present: true,
|
|
4802
|
+
sessions: sessions.map(parseLegacySession)
|
|
4803
|
+
};
|
|
4804
|
+
}
|
|
4805
|
+
async function inspectLegacyHomes(path) {
|
|
4806
|
+
let stats;
|
|
4807
|
+
try {
|
|
4808
|
+
stats = await lstat2(path);
|
|
4809
|
+
} catch (error) {
|
|
4810
|
+
return errorCode8(error) === "ENOENT" ? { present: false, homeCount: 0 } : { present: true, warning: `Could not inspect legacy v1 homes: ${errorMessage(error)}` };
|
|
4811
|
+
}
|
|
4812
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
4813
|
+
return {
|
|
4814
|
+
present: true,
|
|
4815
|
+
warning: "Legacy v1 homes root is not a real directory; it was not traversed."
|
|
4816
|
+
};
|
|
4817
|
+
}
|
|
4818
|
+
try {
|
|
4819
|
+
const entries = await readdir4(path, { withFileTypes: true });
|
|
4820
|
+
return {
|
|
4821
|
+
present: true,
|
|
4822
|
+
homeCount: entries.filter((entry) => entry.isDirectory()).length
|
|
4823
|
+
};
|
|
4824
|
+
} catch (error) {
|
|
4825
|
+
return {
|
|
4826
|
+
present: true,
|
|
4827
|
+
warning: `Could not enumerate legacy v1 homes: ${errorMessage(error)}`
|
|
4828
|
+
};
|
|
4829
|
+
}
|
|
4830
|
+
}
|
|
4831
|
+
function shellQuoteForCommand(value) {
|
|
4832
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
4833
|
+
}
|
|
4834
|
+
async function findLegacyArtifacts() {
|
|
4835
|
+
const statePath = join4(saptoolsDir(), LEGACY_STATE_FILENAME);
|
|
4836
|
+
const lockPath = join4(saptoolsDir(), LEGACY_LOCK_FILENAME);
|
|
4837
|
+
const homesPath = join4(saptoolsDir(), LEGACY_HOMES_DIRNAME);
|
|
4838
|
+
const [state, homes] = await Promise.all([
|
|
4839
|
+
inspectLegacyState(statePath),
|
|
4840
|
+
inspectLegacyHomes(homesPath)
|
|
4841
|
+
]);
|
|
4842
|
+
const inspectionWarnings = [state.warning, homes.warning].filter(
|
|
4843
|
+
(warning) => warning !== void 0
|
|
4844
|
+
);
|
|
4845
|
+
const base = {
|
|
4846
|
+
statePath,
|
|
4847
|
+
statePresent: state.present,
|
|
4848
|
+
homesPath,
|
|
4849
|
+
homesPresent: homes.present,
|
|
4850
|
+
...homes.homeCount === void 0 ? {} : { homeCount: homes.homeCount },
|
|
4851
|
+
...state.sessions === void 0 ? {} : { sessions: state.sessions },
|
|
4852
|
+
...inspectionWarnings.length === 0 ? {} : { inspectionWarnings }
|
|
4853
|
+
};
|
|
4854
|
+
if (!state.present && !homes.present) {
|
|
4855
|
+
return base;
|
|
4856
|
+
}
|
|
4857
|
+
return {
|
|
4858
|
+
...base,
|
|
4859
|
+
warning: "Legacy v1 debugger homes may contain live CF refresh and access tokens. Confirm no reported v1 tunnel is running before removing them.",
|
|
4860
|
+
manualRemovalCommand: `rm -rf -- ${shellQuoteForCommand(homesPath)} ${shellQuoteForCommand(statePath)} ${shellQuoteForCommand(lockPath)}`
|
|
4861
|
+
};
|
|
4862
|
+
}
|
|
4863
|
+
|
|
4864
|
+
// src/debug-session/doctor-session-health.ts
|
|
4865
|
+
import { hostname as hostname3 } from "os";
|
|
4866
|
+
import nodeProcess9 from "process";
|
|
4867
|
+
function missingIdentityCaveat(session) {
|
|
4868
|
+
const identity = session.status === "ready" ? session.tunnelProcessIdentity : session.controllerProcessIdentity;
|
|
4869
|
+
return (nodeProcess9.platform === "linux" || nodeProcess9.platform === "darwin") && identity === void 0 ? "process identity token is absent, so PID-only compatibility is in use; an older cf-debugger sharing this state file may have stripped the additive field" : void 0;
|
|
4870
|
+
}
|
|
4871
|
+
async function inspectDoctorSessions(sessions) {
|
|
4872
|
+
const local = sessions.filter((session) => session.hostname === hostname3());
|
|
4873
|
+
return await Promise.all(local.map(async (session) => {
|
|
4874
|
+
try {
|
|
4875
|
+
const health = await inspectSessionHealth(session);
|
|
4876
|
+
const caveat = missingIdentityCaveat(session);
|
|
4877
|
+
return {
|
|
4878
|
+
session,
|
|
4879
|
+
health: caveat === void 0 ? health : { ...health, reason: `${health.reason}; ${caveat}` }
|
|
4880
|
+
};
|
|
4881
|
+
} catch (error) {
|
|
4882
|
+
return {
|
|
4883
|
+
session,
|
|
4884
|
+
health: {
|
|
4885
|
+
status: "unverified",
|
|
4886
|
+
reason: `health inspection failed: ${error instanceof Error ? error.message : String(error)}`
|
|
4887
|
+
}
|
|
4888
|
+
};
|
|
4889
|
+
}
|
|
4890
|
+
}));
|
|
4891
|
+
}
|
|
4892
|
+
|
|
4893
|
+
// src/debug-session/doctor.ts
|
|
4013
4894
|
var MANAGED_PORT_MIN = 2e4;
|
|
4014
4895
|
var MANAGED_PORT_MAX = 20999;
|
|
4015
4896
|
var PORT_SCAN_CONCURRENCY = 32;
|
|
4016
4897
|
var STALE_TEMP_AGE_MS2 = 24 * 60 * 60 * 1e3;
|
|
4017
4898
|
var STALE_LOCK_AGE_MS = 60 * 60 * 1e3;
|
|
4018
4899
|
var FOREIGN_OR_MALFORMED_LOCK_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
4019
|
-
|
|
4020
|
-
var LEGACY_LOCK_FILENAME = "cf-debugger-state.lock";
|
|
4021
|
-
var LEGACY_HOMES_DIRNAME = "cf-debugger-homes";
|
|
4022
|
-
function errorCode7(error) {
|
|
4900
|
+
function errorCode9(error) {
|
|
4023
4901
|
if (typeof error !== "object" || error === null) {
|
|
4024
4902
|
return void 0;
|
|
4025
4903
|
}
|
|
4026
4904
|
const value = Reflect.get(error, "code");
|
|
4027
4905
|
return typeof value === "string" ? value : void 0;
|
|
4028
4906
|
}
|
|
4029
|
-
function
|
|
4907
|
+
function errorMessage2(error) {
|
|
4030
4908
|
return error instanceof Error ? error.message : String(error);
|
|
4031
4909
|
}
|
|
4032
4910
|
function emptyState2() {
|
|
@@ -4035,14 +4913,14 @@ function emptyState2() {
|
|
|
4035
4913
|
async function readDoctorState() {
|
|
4036
4914
|
let raw;
|
|
4037
4915
|
try {
|
|
4038
|
-
raw = await
|
|
4916
|
+
raw = await readFile7(stateFilePath(), "utf8");
|
|
4039
4917
|
} catch (error) {
|
|
4040
|
-
if (
|
|
4918
|
+
if (errorCode9(error) === "ENOENT") {
|
|
4041
4919
|
return { state: emptyState2(), warnings: [], homeCleanupSafe: true };
|
|
4042
4920
|
}
|
|
4043
4921
|
return {
|
|
4044
4922
|
state: emptyState2(),
|
|
4045
|
-
warnings: [`Could not read debugger state: ${
|
|
4923
|
+
warnings: [`Could not read debugger state: ${errorMessage2(error)}`],
|
|
4046
4924
|
homeCleanupSafe: false
|
|
4047
4925
|
};
|
|
4048
4926
|
}
|
|
@@ -4070,54 +4948,58 @@ async function readDoctorState() {
|
|
|
4070
4948
|
homeCleanupSafe: decoded.dropped.length === 0
|
|
4071
4949
|
};
|
|
4072
4950
|
}
|
|
4073
|
-
async function inspectSessions(sessions) {
|
|
4074
|
-
const local = sessions.filter((session) => session.hostname === hostname2());
|
|
4075
|
-
return await Promise.all(local.map(async (session) => {
|
|
4076
|
-
try {
|
|
4077
|
-
return { session, health: await inspectSessionHealth(session) };
|
|
4078
|
-
} catch (error) {
|
|
4079
|
-
return {
|
|
4080
|
-
session,
|
|
4081
|
-
health: {
|
|
4082
|
-
status: "unverified",
|
|
4083
|
-
reason: `health inspection failed: ${errorMessage(error)}`
|
|
4084
|
-
}
|
|
4085
|
-
};
|
|
4086
|
-
}
|
|
4087
|
-
}));
|
|
4088
|
-
}
|
|
4089
4951
|
async function readDirectory(path) {
|
|
4090
4952
|
try {
|
|
4091
|
-
return await
|
|
4953
|
+
return await readdir5(path, { withFileTypes: true });
|
|
4092
4954
|
} catch (error) {
|
|
4093
|
-
if (
|
|
4955
|
+
if (errorCode9(error) === "ENOENT") {
|
|
4094
4956
|
return [];
|
|
4095
4957
|
}
|
|
4096
4958
|
throw error;
|
|
4097
4959
|
}
|
|
4098
4960
|
}
|
|
4099
|
-
async function findOrphanHomes(sessions,
|
|
4100
|
-
const
|
|
4101
|
-
const
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4961
|
+
async function findOrphanHomes(sessions, cleanupRequested, cleanupSafe) {
|
|
4962
|
+
const discovery = await discoverOrphanHomes(sessions);
|
|
4963
|
+
const findings = await Promise.all(discovery.candidates.map(
|
|
4964
|
+
async (candidate) => {
|
|
4965
|
+
const { cleanupEligible, path, reason, sessionId } = candidate;
|
|
4966
|
+
if (!cleanupRequested) {
|
|
4967
|
+
return {
|
|
4968
|
+
sessionId,
|
|
4969
|
+
path,
|
|
4970
|
+
cleanupEligible,
|
|
4971
|
+
cleanupStatus: "not-requested",
|
|
4972
|
+
...reason === void 0 ? {} : { reason }
|
|
4973
|
+
};
|
|
4974
|
+
}
|
|
4975
|
+
if (!cleanupSafe) {
|
|
4976
|
+
return {
|
|
4977
|
+
sessionId,
|
|
4978
|
+
path,
|
|
4979
|
+
cleanupEligible,
|
|
4980
|
+
cleanupStatus: cleanupEligible ? "skipped" : "not-eligible",
|
|
4981
|
+
...reason === void 0 ? {} : { reason }
|
|
4982
|
+
};
|
|
4983
|
+
}
|
|
4984
|
+
if (!cleanupEligible) {
|
|
4985
|
+
return {
|
|
4986
|
+
sessionId,
|
|
4987
|
+
path,
|
|
4988
|
+
cleanupEligible,
|
|
4989
|
+
cleanupStatus: "not-eligible",
|
|
4990
|
+
...reason === void 0 ? {} : { reason }
|
|
4991
|
+
};
|
|
4992
|
+
}
|
|
4993
|
+
const cleanupStatus = await cleanupOrphanHome(sessionId, path);
|
|
4994
|
+
return {
|
|
4995
|
+
sessionId,
|
|
4996
|
+
path,
|
|
4997
|
+
cleanupEligible,
|
|
4998
|
+
cleanupStatus
|
|
4999
|
+
};
|
|
4112
5000
|
}
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
sessionId: entry.name,
|
|
4116
|
-
path,
|
|
4117
|
-
cleanupEligible,
|
|
4118
|
-
cleanupStatus
|
|
4119
|
-
};
|
|
4120
|
-
}));
|
|
5001
|
+
));
|
|
5002
|
+
return { findings, warnings: discovery.warnings };
|
|
4121
5003
|
}
|
|
4122
5004
|
async function cleanupOrphanHome(sessionId, path) {
|
|
4123
5005
|
try {
|
|
@@ -4158,7 +5040,7 @@ async function mapConcurrent(values, concurrency, work) {
|
|
|
4158
5040
|
}
|
|
4159
5041
|
function unclaimedManagedPorts(sessions) {
|
|
4160
5042
|
const localClaims = new Set(
|
|
4161
|
-
sessions.filter((session) => session.hostname ===
|
|
5043
|
+
sessions.filter((session) => session.hostname === hostname4()).map((session) => session.localPort)
|
|
4162
5044
|
);
|
|
4163
5045
|
const ports = [];
|
|
4164
5046
|
for (let port = MANAGED_PORT_MIN; port <= MANAGED_PORT_MAX; port += 1) {
|
|
@@ -4235,15 +5117,15 @@ function parseLockOwner2(raw) {
|
|
|
4235
5117
|
}
|
|
4236
5118
|
function isPidAlive2(pid) {
|
|
4237
5119
|
try {
|
|
4238
|
-
|
|
5120
|
+
nodeProcess10.kill(pid, 0);
|
|
4239
5121
|
return true;
|
|
4240
5122
|
} catch (error) {
|
|
4241
|
-
return
|
|
5123
|
+
return errorCode9(error) !== "ESRCH";
|
|
4242
5124
|
}
|
|
4243
5125
|
}
|
|
4244
5126
|
async function readLockOwner2(path) {
|
|
4245
5127
|
try {
|
|
4246
|
-
return parseLockOwner2(await
|
|
5128
|
+
return parseLockOwner2(await readFile7(path, "utf8"));
|
|
4247
5129
|
} catch {
|
|
4248
5130
|
return void 0;
|
|
4249
5131
|
}
|
|
@@ -4253,7 +5135,7 @@ async function lockCleanupEligible(path, ageMs) {
|
|
|
4253
5135
|
return false;
|
|
4254
5136
|
}
|
|
4255
5137
|
const owner = await readLockOwner2(path);
|
|
4256
|
-
if (owner?.hostname ===
|
|
5138
|
+
if (owner?.hostname === hostname4()) {
|
|
4257
5139
|
if (!isPidAlive2(owner.pid)) {
|
|
4258
5140
|
return true;
|
|
4259
5141
|
}
|
|
@@ -4283,18 +5165,21 @@ async function inspectArtifact(entry, now, claimedSessionIds, stopIntentCleanupS
|
|
|
4283
5165
|
if (kind === void 0) {
|
|
4284
5166
|
return void 0;
|
|
4285
5167
|
}
|
|
4286
|
-
const path =
|
|
4287
|
-
const stats = await
|
|
5168
|
+
const path = join5(saptoolsDir(), entry.name);
|
|
5169
|
+
const stats = await lstat3(path);
|
|
4288
5170
|
const ageMs = Math.max(0, now - stats.mtimeMs);
|
|
4289
5171
|
const regularFile = stats.isFile();
|
|
4290
5172
|
let sessionId;
|
|
4291
5173
|
let cleanupEligible = regularFile && kind === "state-temp" && ageMs >= STALE_TEMP_AGE_MS2;
|
|
5174
|
+
let cleanupBlocked = false;
|
|
4292
5175
|
if (regularFile && kind === "stop-intent") {
|
|
4293
5176
|
sessionId = entry.name.slice(
|
|
4294
5177
|
CF_DEBUGGER_STOP_INTENT_PREFIX.length,
|
|
4295
5178
|
-".stop".length
|
|
4296
5179
|
);
|
|
4297
|
-
|
|
5180
|
+
const otherwiseEligible = ageMs >= STALE_TEMP_AGE_MS2 && !claimedSessionIds.has(sessionId);
|
|
5181
|
+
cleanupEligible = stopIntentCleanupSafe && otherwiseEligible;
|
|
5182
|
+
cleanupBlocked = !stopIntentCleanupSafe && otherwiseEligible;
|
|
4298
5183
|
}
|
|
4299
5184
|
if (regularFile && (kind === "state-lock" || kind === "state-recovery")) {
|
|
4300
5185
|
cleanupEligible = await lockCleanupEligible(path, ageMs);
|
|
@@ -4304,13 +5189,14 @@ async function inspectArtifact(entry, now, claimedSessionIds, stopIntentCleanupS
|
|
|
4304
5189
|
path,
|
|
4305
5190
|
ageMs,
|
|
4306
5191
|
cleanupEligible,
|
|
5192
|
+
cleanupBlocked,
|
|
4307
5193
|
fingerprint: artifactFingerprint(stats),
|
|
4308
5194
|
...sessionId === void 0 ? {} : { sessionId }
|
|
4309
5195
|
};
|
|
4310
5196
|
}
|
|
4311
5197
|
async function candidateStillMatches(candidate) {
|
|
4312
5198
|
try {
|
|
4313
|
-
const stats = await
|
|
5199
|
+
const stats = await lstat3(candidate.path);
|
|
4314
5200
|
return artifactFingerprint(stats) === candidate.fingerprint;
|
|
4315
5201
|
} catch {
|
|
4316
5202
|
return false;
|
|
@@ -4342,11 +5228,19 @@ async function cleanupArtifact(candidate) {
|
|
|
4342
5228
|
await unlink5(candidate.path);
|
|
4343
5229
|
return { status: "removed" };
|
|
4344
5230
|
} catch (error) {
|
|
4345
|
-
if (
|
|
5231
|
+
if (errorCode9(error) === "ENOENT") {
|
|
4346
5232
|
return { status: "skipped" };
|
|
4347
5233
|
}
|
|
4348
|
-
return { status: "failed", error:
|
|
5234
|
+
return { status: "failed", error: errorMessage2(error) };
|
|
5235
|
+
}
|
|
5236
|
+
}
|
|
5237
|
+
async function resolveArtifactCleanup(candidate, cleanup) {
|
|
5238
|
+
if (!cleanup) {
|
|
5239
|
+
return {
|
|
5240
|
+
status: candidate.cleanupEligible ? "not-requested" : "not-eligible"
|
|
5241
|
+
};
|
|
4349
5242
|
}
|
|
5243
|
+
return candidate.cleanupBlocked ? { status: "skipped" } : await cleanupArtifact(candidate);
|
|
4350
5244
|
}
|
|
4351
5245
|
async function findArtifacts(cleanup, sessions, stopIntentCleanupSafe) {
|
|
4352
5246
|
const entries = await readDirectory(saptoolsDir());
|
|
@@ -4361,55 +5255,22 @@ async function findArtifacts(cleanup, sessions, stopIntentCleanupSafe) {
|
|
|
4361
5255
|
).sort((left, right) => left.path.localeCompare(right.path));
|
|
4362
5256
|
const findings = [];
|
|
4363
5257
|
for (const candidate of candidates) {
|
|
4364
|
-
const result =
|
|
4365
|
-
status: candidate.cleanupEligible ? "not-requested" : "not-eligible"
|
|
4366
|
-
};
|
|
5258
|
+
const result = await resolveArtifactCleanup(candidate, cleanup);
|
|
4367
5259
|
findings.push({
|
|
4368
5260
|
kind: candidate.kind,
|
|
4369
5261
|
path: candidate.path,
|
|
4370
5262
|
ageMs: candidate.ageMs,
|
|
4371
5263
|
cleanupEligible: candidate.cleanupEligible,
|
|
4372
5264
|
cleanupStatus: result.status,
|
|
4373
|
-
...result.error === void 0 ? {} : { cleanupError: result.error }
|
|
5265
|
+
...result.error === void 0 ? {} : { cleanupError: result.error },
|
|
5266
|
+
...candidate.kind === "corrupt-backup" ? {
|
|
5267
|
+
note: "Preserved recovery evidence; it may contain debugger session metadata such as target names, PIDs, ports, and CF home paths. Inspect it before manual removal.",
|
|
5268
|
+
manualRemovalCommand: `rm -- ${shellQuoteForCommand(candidate.path)}`
|
|
5269
|
+
} : {}
|
|
4374
5270
|
});
|
|
4375
5271
|
}
|
|
4376
5272
|
return findings;
|
|
4377
5273
|
}
|
|
4378
|
-
async function pathExists(path) {
|
|
4379
|
-
try {
|
|
4380
|
-
await lstat(path);
|
|
4381
|
-
return true;
|
|
4382
|
-
} catch (error) {
|
|
4383
|
-
if (errorCode7(error) === "ENOENT") {
|
|
4384
|
-
return false;
|
|
4385
|
-
}
|
|
4386
|
-
throw error;
|
|
4387
|
-
}
|
|
4388
|
-
}
|
|
4389
|
-
function shellQuote(value) {
|
|
4390
|
-
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
4391
|
-
}
|
|
4392
|
-
async function findLegacyArtifacts() {
|
|
4393
|
-
const statePath = join3(saptoolsDir(), LEGACY_STATE_FILENAME);
|
|
4394
|
-
const lockPath = join3(saptoolsDir(), LEGACY_LOCK_FILENAME);
|
|
4395
|
-
const homesPath = join3(saptoolsDir(), LEGACY_HOMES_DIRNAME);
|
|
4396
|
-
const [statePresent, homesPresent] = await Promise.all([
|
|
4397
|
-
pathExists(statePath),
|
|
4398
|
-
pathExists(homesPath)
|
|
4399
|
-
]);
|
|
4400
|
-
if (!statePresent && !homesPresent) {
|
|
4401
|
-
return { statePath, statePresent, homesPath, homesPresent };
|
|
4402
|
-
}
|
|
4403
|
-
const command = `rm -rf -- ${shellQuote(homesPath)} ${shellQuote(statePath)} ${shellQuote(lockPath)}`;
|
|
4404
|
-
return {
|
|
4405
|
-
statePath,
|
|
4406
|
-
statePresent,
|
|
4407
|
-
homesPath,
|
|
4408
|
-
homesPresent,
|
|
4409
|
-
warning: "Legacy v1 debugger homes may contain live CF refresh and access tokens. Confirm no v1 tunnel is running before removing them.",
|
|
4410
|
-
manualRemovalCommand: command
|
|
4411
|
-
};
|
|
4412
|
-
}
|
|
4413
5274
|
function reportWarnings(stateWarnings, orphanHomes, unclaimedPorts, legacy) {
|
|
4414
5275
|
const warnings = [...stateWarnings];
|
|
4415
5276
|
if (orphanHomes.length > 0) {
|
|
@@ -4426,14 +5287,14 @@ function reportWarnings(stateWarnings, orphanHomes, unclaimedPorts, legacy) {
|
|
|
4426
5287
|
async function runDoctor(options = {}) {
|
|
4427
5288
|
const cleanup = options.cleanup === true;
|
|
4428
5289
|
const stateResult = await readDoctorState();
|
|
4429
|
-
const
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
findOrphanHomes(stateResult.state.sessions, homeCleanup),
|
|
5290
|
+
const [sessions, homeInspection, unclaimedPorts, artifacts, legacy] = await Promise.all([
|
|
5291
|
+
inspectDoctorSessions(stateResult.state.sessions),
|
|
5292
|
+
findOrphanHomes(stateResult.state.sessions, cleanup, stateResult.homeCleanupSafe),
|
|
4433
5293
|
findUnclaimedPorts(stateResult.state.sessions),
|
|
4434
5294
|
findArtifacts(cleanup, stateResult.state.sessions, stateResult.homeCleanupSafe),
|
|
4435
5295
|
findLegacyArtifacts()
|
|
4436
5296
|
]);
|
|
5297
|
+
const orphanHomes = homeInspection.findings;
|
|
4437
5298
|
const cleanedPaths = [
|
|
4438
5299
|
...orphanHomes.filter((finding) => finding.cleanupStatus === "removed").map((finding) => finding.path),
|
|
4439
5300
|
...artifacts.filter((finding) => finding.cleanupStatus === "removed").map((finding) => finding.path)
|
|
@@ -4446,6 +5307,7 @@ async function runDoctor(options = {}) {
|
|
|
4446
5307
|
legacy,
|
|
4447
5308
|
warnings: [
|
|
4448
5309
|
...reportWarnings(stateResult.warnings, orphanHomes, unclaimedPorts, legacy),
|
|
5310
|
+
...homeInspection.warnings,
|
|
4449
5311
|
...cleanup && !stateResult.homeCleanupSafe ? [
|
|
4450
5312
|
"Skipped orphan-home and stop-intent cleanup because debugger state was incomplete or invalid."
|
|
4451
5313
|
] : []
|
|
@@ -4456,7 +5318,7 @@ async function runDoctor(options = {}) {
|
|
|
4456
5318
|
|
|
4457
5319
|
// src/debug-session/sessions.ts
|
|
4458
5320
|
import { hostname as getHostname4 } from "os";
|
|
4459
|
-
import
|
|
5321
|
+
import nodeProcess11 from "process";
|
|
4460
5322
|
function findMatchingSession(sessions, options) {
|
|
4461
5323
|
if (options.sessionId !== void 0) {
|
|
4462
5324
|
return sessions.find((session) => session.sessionId === options.sessionId);
|
|
@@ -4474,50 +5336,55 @@ function findMatchingSession(sessions, options) {
|
|
|
4474
5336
|
}
|
|
4475
5337
|
return matches[0];
|
|
4476
5338
|
}
|
|
4477
|
-
function startupAgeLimit2(session) {
|
|
4478
|
-
return (session.startupTimeoutMs ?? MAX_STARTUP_TIMEOUT_MS) + STARTUP_STALE_SLACK_MS;
|
|
4479
|
-
}
|
|
4480
|
-
function startupExpired2(session) {
|
|
4481
|
-
const startedAt = Date.parse(session.startedAt);
|
|
4482
|
-
return Number.isNaN(startedAt) || Date.now() - startedAt > startupAgeLimit2(session);
|
|
4483
|
-
}
|
|
4484
|
-
async function inspectRecordedProcess2(pid, identity) {
|
|
4485
|
-
if (!isPidAlive(pid)) {
|
|
4486
|
-
return "dead";
|
|
4487
|
-
}
|
|
4488
|
-
return await inspectProcessIdentity(pid, identity);
|
|
4489
|
-
}
|
|
4490
5339
|
async function inspectController(target) {
|
|
4491
5340
|
const controllerPid = target.controllerPid ?? target.pid;
|
|
4492
|
-
return await
|
|
5341
|
+
return await inspectRecordedProcess(
|
|
5342
|
+
controllerPid,
|
|
5343
|
+
target.controllerProcessIdentity,
|
|
5344
|
+
isPidAlive
|
|
5345
|
+
);
|
|
4493
5346
|
}
|
|
4494
5347
|
async function ownsRecordedTunnel(target) {
|
|
4495
5348
|
const tunnelPid = target.tunnelPid;
|
|
4496
5349
|
if (tunnelPid === void 0) {
|
|
4497
5350
|
return false;
|
|
4498
5351
|
}
|
|
4499
|
-
const processVerdict = await
|
|
5352
|
+
const processVerdict = await inspectRecordedProcess(
|
|
4500
5353
|
tunnelPid,
|
|
4501
|
-
target.tunnelProcessIdentity
|
|
5354
|
+
target.tunnelProcessIdentity,
|
|
5355
|
+
isPidAlive
|
|
4502
5356
|
);
|
|
4503
5357
|
if (processVerdict !== "match") {
|
|
4504
5358
|
return false;
|
|
4505
5359
|
}
|
|
4506
5360
|
return (await inspectPortOwnership(target.localPort, tunnelPid)).status === "owned";
|
|
4507
5361
|
}
|
|
4508
|
-
async function terminateVerifiedTunnel(target) {
|
|
5362
|
+
async function terminateVerifiedTunnel(target, recordExpectedStop) {
|
|
4509
5363
|
const tunnelPid = target.tunnelPid;
|
|
4510
|
-
if (tunnelPid === void 0 || tunnelPid ===
|
|
4511
|
-
return tunnelPid ===
|
|
5364
|
+
if (tunnelPid === void 0 || tunnelPid === nodeProcess11.pid) {
|
|
5365
|
+
return tunnelPid === nodeProcess11.pid ? "still-alive" : "terminated";
|
|
4512
5366
|
}
|
|
5367
|
+
let stopIntentRecorded = false;
|
|
4513
5368
|
try {
|
|
4514
5369
|
const verifyBeforeSignal = async (signal) => {
|
|
4515
5370
|
if (signal === "SIGTERM" || target.tunnelProcessIdentity === void 0) {
|
|
4516
|
-
|
|
5371
|
+
if (!await ownsRecordedTunnel(target)) {
|
|
5372
|
+
return false;
|
|
5373
|
+
}
|
|
5374
|
+
if (recordExpectedStop && !stopIntentRecorded) {
|
|
5375
|
+
const claim = await requestSessionStop(target.sessionId);
|
|
5376
|
+
if (claim === void 0) {
|
|
5377
|
+
return false;
|
|
5378
|
+
}
|
|
5379
|
+
stopIntentRecorded = true;
|
|
5380
|
+
return await ownsRecordedTunnel(target);
|
|
5381
|
+
}
|
|
5382
|
+
return true;
|
|
4517
5383
|
}
|
|
4518
|
-
return await
|
|
5384
|
+
return await inspectRecordedProcess(
|
|
4519
5385
|
tunnelPid,
|
|
4520
|
-
target.tunnelProcessIdentity
|
|
5386
|
+
target.tunnelProcessIdentity,
|
|
5387
|
+
isPidAlive
|
|
4521
5388
|
) === "match";
|
|
4522
5389
|
};
|
|
4523
5390
|
return await terminatePidOrGroup(
|
|
@@ -4531,13 +5398,10 @@ async function terminateVerifiedTunnel(target) {
|
|
|
4531
5398
|
}
|
|
4532
5399
|
}
|
|
4533
5400
|
async function terminateVerifiedTunnelAndConfirm(target, recordExpectedStop = false) {
|
|
4534
|
-
if (recordExpectedStop) {
|
|
4535
|
-
await writeSessionStopIntent(target.sessionId);
|
|
4536
|
-
}
|
|
4537
5401
|
if (!await ownsRecordedTunnel(target)) {
|
|
4538
5402
|
throw ownershipError(target, "recorded tunnel no longer owns the local port");
|
|
4539
5403
|
}
|
|
4540
|
-
const termination = await terminateVerifiedTunnel(target);
|
|
5404
|
+
const termination = await terminateVerifiedTunnel(target, recordExpectedStop);
|
|
4541
5405
|
if (termination !== "terminated" || await ownsRecordedTunnel(target)) {
|
|
4542
5406
|
throw new CfDebuggerError(
|
|
4543
5407
|
"TUNNEL_TERMINATION_FAILED",
|
|
@@ -4547,18 +5411,26 @@ async function terminateVerifiedTunnelAndConfirm(target, recordExpectedStop = fa
|
|
|
4547
5411
|
}
|
|
4548
5412
|
async function removeOwnedSession(target, stale, forced = false, warning, preserveStopIntent = false) {
|
|
4549
5413
|
let resultWarning = warning;
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
5414
|
+
const cleanup = await forgetSessionThenCleanupHome(target);
|
|
5415
|
+
if (cleanup.homeStatus === "retained") {
|
|
5416
|
+
const retained = `The session record was removed, but its owned CF home ${target.cfHomeDir} could not be deleted and may still hold a live CF refresh token; remove it manually or run \`cf-debugger doctor --cleanup\`.`;
|
|
5417
|
+
resultWarning = appendWarning(resultWarning, retained);
|
|
5418
|
+
} else if (cleanup.homeStatus === "unowned") {
|
|
4553
5419
|
const skipped = `State referenced unowned CF home ${target.cfHomeDir}; it was not deleted.`;
|
|
4554
|
-
resultWarning = resultWarning
|
|
5420
|
+
resultWarning = appendWarning(resultWarning, skipped);
|
|
4555
5421
|
}
|
|
4556
|
-
const removed = await removeSession(target.sessionId);
|
|
4557
5422
|
if (!preserveStopIntent) {
|
|
4558
|
-
|
|
5423
|
+
try {
|
|
5424
|
+
await clearSessionStopIntent(target.sessionId);
|
|
5425
|
+
} catch {
|
|
5426
|
+
resultWarning = appendWarning(
|
|
5427
|
+
resultWarning,
|
|
5428
|
+
`The session record was removed, but its stop-intent artifact could not be deleted; run \`cf-debugger doctor --cleanup\` after resolving the filesystem error.`
|
|
5429
|
+
);
|
|
5430
|
+
}
|
|
4559
5431
|
}
|
|
4560
5432
|
return {
|
|
4561
|
-
...removed ?? target,
|
|
5433
|
+
...cleanup.removed ?? target,
|
|
4562
5434
|
stale,
|
|
4563
5435
|
pending: false,
|
|
4564
5436
|
forced,
|
|
@@ -4574,6 +5446,9 @@ function ownershipError(target, detail) {
|
|
|
4574
5446
|
function forcedWarning(target, detail) {
|
|
4575
5447
|
return `Forced state cleanup abandoned PID ${String(target.tunnelPid ?? target.controllerPid ?? target.pid)} and local port ${target.localPort.toString()}: ${detail}. No unverified process was signalled.`;
|
|
4576
5448
|
}
|
|
5449
|
+
function appendWarning(current, next) {
|
|
5450
|
+
return current === void 0 ? next : `${current} ${next}`;
|
|
5451
|
+
}
|
|
4577
5452
|
async function forceRemoveUnverified(target, detail, preserveStopIntent = false) {
|
|
4578
5453
|
return await removeOwnedSession(
|
|
4579
5454
|
target,
|
|
@@ -4588,20 +5463,24 @@ async function stopReadySession(target, force) {
|
|
|
4588
5463
|
await terminateVerifiedTunnelAndConfirm(target, true);
|
|
4589
5464
|
return await removeOwnedSession(target, false, false, void 0, true);
|
|
4590
5465
|
}
|
|
4591
|
-
const tunnelVerdict = target.tunnelPid === void 0 ? "dead" : await
|
|
5466
|
+
const tunnelVerdict = target.tunnelPid === void 0 ? "dead" : await inspectRecordedProcess(
|
|
5467
|
+
target.tunnelPid,
|
|
5468
|
+
target.tunnelProcessIdentity,
|
|
5469
|
+
isPidAlive
|
|
5470
|
+
);
|
|
4592
5471
|
const tunnelDead = target.tunnelPid === void 0 || !isPidOrGroupAlive(target.tunnelPid) || tunnelVerdict === "mismatch";
|
|
4593
5472
|
const ownership = target.tunnelPid === void 0 ? await inspectListeningProcesses(target.localPort) : await inspectPortOwnership(target.localPort, target.tunnelPid);
|
|
4594
5473
|
if (tunnelDead && ownership.status === "not-listening") {
|
|
4595
|
-
return await removeOwnedSession(target, true
|
|
5474
|
+
return await removeOwnedSession(target, true);
|
|
4596
5475
|
}
|
|
4597
5476
|
const detail = ownership.status === "unverified" ? ownership.reason : ownership.status === "not-owned" ? `local port ${target.localPort.toString()} belongs to PID(s) ${ownership.pids.join(", ")}` : tunnelVerdict === "unavailable" ? "the recorded tunnel identity could not be inspected" : "the recorded tunnel could not be proven dead and owned";
|
|
4598
5477
|
if (force) {
|
|
4599
|
-
return await forceRemoveUnverified(target, detail
|
|
5478
|
+
return await forceRemoveUnverified(target, detail);
|
|
4600
5479
|
}
|
|
4601
5480
|
throw ownershipError(target, detail);
|
|
4602
5481
|
}
|
|
4603
5482
|
async function stopStartingSession(target, force) {
|
|
4604
|
-
const expired =
|
|
5483
|
+
const expired = startupExpired(target);
|
|
4605
5484
|
const controllerVerdict = await inspectController(target);
|
|
4606
5485
|
if (!force && !expired && controllerVerdict === "match") {
|
|
4607
5486
|
return { ...target, stale: false, pending: true, forced: false };
|
|
@@ -4633,17 +5512,20 @@ async function stopDebugger(options) {
|
|
|
4633
5512
|
if (target === void 0) {
|
|
4634
5513
|
return void 0;
|
|
4635
5514
|
}
|
|
5515
|
+
if (target.status === "ready") {
|
|
5516
|
+
return await stopReadySession(target, options.force === true);
|
|
5517
|
+
}
|
|
4636
5518
|
const claim = await requestSessionStop(target.sessionId);
|
|
4637
5519
|
if (claim === void 0) {
|
|
4638
5520
|
return void 0;
|
|
4639
5521
|
}
|
|
4640
|
-
return
|
|
5522
|
+
return await stopStartingSession(claim.session, options.force === true);
|
|
4641
5523
|
}
|
|
4642
5524
|
function outcomeForResult(result) {
|
|
4643
5525
|
return {
|
|
4644
5526
|
sessionId: result.sessionId,
|
|
4645
5527
|
app: result.app,
|
|
4646
|
-
status: result.pending ? "pending" : result.stale ? "stale" : "stopped",
|
|
5528
|
+
status: result.pending ? "pending" : result.forced ? "forced" : result.stale ? "stale" : "stopped",
|
|
4647
5529
|
result
|
|
4648
5530
|
};
|
|
4649
5531
|
}
|
|
@@ -4671,6 +5553,7 @@ function summarizeOutcomes(outcomes) {
|
|
|
4671
5553
|
return {
|
|
4672
5554
|
outcomes,
|
|
4673
5555
|
failed: count("failed"),
|
|
5556
|
+
forced: count("forced"),
|
|
4674
5557
|
pending: count("pending"),
|
|
4675
5558
|
stale: count("stale"),
|
|
4676
5559
|
stopped: count("stopped")
|
|
@@ -4703,6 +5586,7 @@ async function getSession(key) {
|
|
|
4703
5586
|
|
|
4704
5587
|
export {
|
|
4705
5588
|
CfDebuggerError,
|
|
5589
|
+
validateApiEndpointOverride,
|
|
4706
5590
|
resolveApiEndpoint,
|
|
4707
5591
|
listKnownRegionKeys,
|
|
4708
5592
|
readCurrentCfTarget,
|
|
@@ -4714,10 +5598,12 @@ export {
|
|
|
4714
5598
|
resolveNodeTarget,
|
|
4715
5599
|
buildNodeInspectorCommand,
|
|
4716
5600
|
parseNodeInspectorMarkers,
|
|
5601
|
+
parseRestartEnvironment,
|
|
4717
5602
|
sessionKeyString,
|
|
4718
5603
|
CLEANUP_FAILURE_EXIT_CODE,
|
|
4719
5604
|
hasTunnelTerminationFailure,
|
|
4720
5605
|
cliErrorExitCode,
|
|
5606
|
+
stopAllExitCode,
|
|
4721
5607
|
getDebuggerHandleTunnelError,
|
|
4722
5608
|
resolveStartupTimeoutMs,
|
|
4723
5609
|
remainingStartupMs,
|
|
@@ -4731,4 +5617,4 @@ export {
|
|
|
4731
5617
|
listSessions,
|
|
4732
5618
|
getSession
|
|
4733
5619
|
};
|
|
4734
|
-
//# sourceMappingURL=chunk-
|
|
5620
|
+
//# sourceMappingURL=chunk-QBB4N2WN.js.map
|