@saptools/cf-debugger 0.2.1 → 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-AKQR4Y74.js → chunk-QBB4N2WN.js} +1509 -689
- 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-AKQR4Y74.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,16 +982,124 @@ 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";
|
|
851
988
|
|
|
989
|
+
// src/cloud-foundry/ssh-shared.ts
|
|
990
|
+
var DEFAULT_MAX_OUTPUT_BYTES = 65536;
|
|
991
|
+
var LIVE_LINE_LIMIT_BYTES = 65536;
|
|
992
|
+
var MAX_REDACTION_OVERLAP_BYTES = 4096;
|
|
993
|
+
var SENSITIVE_OUTPUT_OMITTED = "[diagnostic output omitted to protect a sensitive value]";
|
|
994
|
+
function buildCfSshArgs(appName, target, tail) {
|
|
995
|
+
const resolved = resolveNodeTarget(target);
|
|
996
|
+
const processArgs = resolved.process === DEFAULT_CF_PROCESS ? [] : ["--process", resolved.process];
|
|
997
|
+
return [
|
|
998
|
+
"ssh",
|
|
999
|
+
appName,
|
|
1000
|
+
...processArgs,
|
|
1001
|
+
"-i",
|
|
1002
|
+
resolved.instance.toString(),
|
|
1003
|
+
...tail
|
|
1004
|
+
];
|
|
1005
|
+
}
|
|
1006
|
+
function createBoundedOutput() {
|
|
1007
|
+
return { chunks: [], bytes: 0, truncated: false };
|
|
1008
|
+
}
|
|
1009
|
+
function appendHead(output, data, outputLimit, retainedLimit) {
|
|
1010
|
+
const incoming = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
1011
|
+
if (incoming.byteLength === 0) {
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
output.truncated ||= output.bytes + incoming.byteLength > outputLimit;
|
|
1015
|
+
const remaining = Math.max(0, retainedLimit - output.bytes);
|
|
1016
|
+
if (remaining === 0) {
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
const next = incoming.byteLength <= remaining ? incoming : Buffer.from(incoming.subarray(0, remaining));
|
|
1020
|
+
output.chunks.push(next);
|
|
1021
|
+
output.bytes += next.byteLength;
|
|
1022
|
+
}
|
|
1023
|
+
function appendTail(output, data, outputLimit, retainedLimit) {
|
|
1024
|
+
const incoming = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
1025
|
+
if (incoming.byteLength === 0) {
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
output.chunks.push(incoming);
|
|
1029
|
+
output.bytes += incoming.byteLength;
|
|
1030
|
+
output.truncated ||= output.bytes > outputLimit;
|
|
1031
|
+
while (output.bytes > retainedLimit && output.chunks.length > 0) {
|
|
1032
|
+
const first = output.chunks[0];
|
|
1033
|
+
if (first === void 0) {
|
|
1034
|
+
break;
|
|
1035
|
+
}
|
|
1036
|
+
const excess = output.bytes - retainedLimit;
|
|
1037
|
+
if (first.byteLength <= excess) {
|
|
1038
|
+
output.chunks.shift();
|
|
1039
|
+
output.bytes -= first.byteLength;
|
|
1040
|
+
} else {
|
|
1041
|
+
output.chunks[0] = Buffer.from(first.subarray(excess));
|
|
1042
|
+
output.bytes -= excess;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
function createRedactionPolicy(values) {
|
|
1047
|
+
const sensitiveValues = normalizeSensitiveValues(values);
|
|
1048
|
+
const overlapBytes = sensitiveValues.reduce(
|
|
1049
|
+
(largest, value) => Math.max(largest, Buffer.byteLength(value)),
|
|
1050
|
+
0
|
|
1051
|
+
);
|
|
1052
|
+
return {
|
|
1053
|
+
safeToSurface: overlapBytes <= MAX_REDACTION_OVERLAP_BYTES && !sensitiveValues.some((value) => /[\r\n]/.test(value)),
|
|
1054
|
+
sensitiveValues,
|
|
1055
|
+
overlapBytes: Math.min(overlapBytes, MAX_REDACTION_OVERLAP_BYTES)
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
function outputText(output) {
|
|
1059
|
+
return Buffer.concat(output.chunks, output.bytes).toString("utf8");
|
|
1060
|
+
}
|
|
1061
|
+
function limitText(text, limit, keep) {
|
|
1062
|
+
const buffer = Buffer.from(text);
|
|
1063
|
+
if (buffer.byteLength <= limit) {
|
|
1064
|
+
return text;
|
|
1065
|
+
}
|
|
1066
|
+
const limited = keep === "head" ? buffer.subarray(0, limit) : buffer.subarray(buffer.byteLength - limit);
|
|
1067
|
+
return limited.toString("utf8");
|
|
1068
|
+
}
|
|
1069
|
+
function safeOutputText(output, policy, limit, keep) {
|
|
1070
|
+
if (!policy.safeToSurface && output.bytes > 0) {
|
|
1071
|
+
return SENSITIVE_OUTPUT_OMITTED;
|
|
1072
|
+
}
|
|
1073
|
+
const redacted = redactSensitiveText(outputText(output), policy.sensitiveValues);
|
|
1074
|
+
return limitText(redacted, limit, keep);
|
|
1075
|
+
}
|
|
1076
|
+
function sshAbortError(context) {
|
|
1077
|
+
if (context.deadlineAt !== void 0 && Date.now() >= context.deadlineAt) {
|
|
1078
|
+
return new CfDebuggerError(
|
|
1079
|
+
"STARTUP_TIMEOUT",
|
|
1080
|
+
`Debugger startup exceeded its configured deadline during ${context.phase ?? "remote signalling"}.`
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
return new CfDebuggerError("ABORTED", "Operation aborted by caller");
|
|
1084
|
+
}
|
|
1085
|
+
function isDeadlineExpired(context) {
|
|
1086
|
+
return context.deadlineAt !== void 0 && Date.now() >= context.deadlineAt;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// src/cloud-foundry/ssh-one-shot.ts
|
|
1090
|
+
import {
|
|
1091
|
+
spawn
|
|
1092
|
+
} from "child_process";
|
|
1093
|
+
import nodeProcess3 from "process";
|
|
1094
|
+
|
|
852
1095
|
// src/debug-session/process-identity.ts
|
|
853
1096
|
import { execFile as execFile2 } from "child_process";
|
|
854
1097
|
import { readFile } from "fs/promises";
|
|
855
1098
|
import nodeProcess2 from "process";
|
|
856
1099
|
import { promisify } from "util";
|
|
857
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}$/;
|
|
858
1103
|
function errorCode(error) {
|
|
859
1104
|
if (typeof error !== "object" || error === null) {
|
|
860
1105
|
return void 0;
|
|
@@ -883,7 +1128,7 @@ async function linuxProcessIdentity(pid, signal) {
|
|
|
883
1128
|
...signal === void 0 ? {} : { signal }
|
|
884
1129
|
});
|
|
885
1130
|
const startTime = parseLinuxProcessStartTime(statLine);
|
|
886
|
-
return startTime === void 0 ? void 0 : `linux:${startTime}`;
|
|
1131
|
+
return startTime === void 0 ? void 0 : `linux:${PROCESS_IDENTITY_VERSION}:${startTime}`;
|
|
887
1132
|
} catch (error) {
|
|
888
1133
|
throwIfAborted(signal);
|
|
889
1134
|
if (errorCode(error) === "ENOENT") {
|
|
@@ -892,14 +1137,22 @@ async function linuxProcessIdentity(pid, signal) {
|
|
|
892
1137
|
return void 0;
|
|
893
1138
|
}
|
|
894
1139
|
}
|
|
1140
|
+
function darwinIdentityEnvironment(environment) {
|
|
1141
|
+
return {
|
|
1142
|
+
...environment,
|
|
1143
|
+
LC_ALL: "C",
|
|
1144
|
+
TZ: "UTC"
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
895
1147
|
async function darwinProcessIdentity(pid, signal) {
|
|
896
1148
|
try {
|
|
897
1149
|
const { stdout } = await execFileAsync("ps", ["-p", pid.toString(), "-o", "lstart="], {
|
|
1150
|
+
env: darwinIdentityEnvironment(nodeProcess2.env),
|
|
898
1151
|
...signal === void 0 ? {} : { signal },
|
|
899
1152
|
timeout: 2e3
|
|
900
1153
|
});
|
|
901
1154
|
const startedAt = stdout.trim();
|
|
902
|
-
return startedAt.length === 0 ? void 0 : `darwin:${startedAt}`;
|
|
1155
|
+
return startedAt.length === 0 ? void 0 : `darwin:${PROCESS_IDENTITY_VERSION}:${startedAt}`;
|
|
903
1156
|
} catch {
|
|
904
1157
|
throwIfAborted(signal);
|
|
905
1158
|
return void 0;
|
|
@@ -918,36 +1171,29 @@ async function readProcessIdentity(pid, signal) {
|
|
|
918
1171
|
}
|
|
919
1172
|
return void 0;
|
|
920
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
|
+
}
|
|
921
1181
|
async function inspectProcessIdentity(pid, expected, signal) {
|
|
922
1182
|
throwIfAborted(signal);
|
|
923
1183
|
if (expected === void 0) {
|
|
924
1184
|
return "match";
|
|
925
1185
|
}
|
|
1186
|
+
if (!isCurrentProcessIdentity(expected)) {
|
|
1187
|
+
return "unavailable";
|
|
1188
|
+
}
|
|
926
1189
|
const current = await readProcessIdentity(pid, signal);
|
|
927
|
-
if (current === void 0) {
|
|
1190
|
+
if (current === void 0 || !isCurrentProcessIdentity(current)) {
|
|
928
1191
|
return "unavailable";
|
|
929
1192
|
}
|
|
930
1193
|
return current === expected ? "match" : "mismatch";
|
|
931
1194
|
}
|
|
932
1195
|
|
|
933
|
-
// src/cloud-foundry/ssh.ts
|
|
934
|
-
var DEFAULT_MAX_OUTPUT_BYTES = 65536;
|
|
935
|
-
var LIVE_LINE_LIMIT_BYTES = 65536;
|
|
936
|
-
var MAX_REDACTION_OVERLAP_BYTES = 4096;
|
|
937
|
-
var SENSITIVE_OUTPUT_OMITTED = "[diagnostic output omitted to protect a sensitive value]";
|
|
938
|
-
var tunnelCaptures = /* @__PURE__ */ new WeakMap();
|
|
939
|
-
function buildCfSshArgs(appName, target, tail) {
|
|
940
|
-
const resolved = resolveNodeTarget(target);
|
|
941
|
-
const processArgs = resolved.process === DEFAULT_CF_PROCESS ? [] : ["--process", resolved.process];
|
|
942
|
-
return [
|
|
943
|
-
"ssh",
|
|
944
|
-
appName,
|
|
945
|
-
...processArgs,
|
|
946
|
-
"-i",
|
|
947
|
-
resolved.instance.toString(),
|
|
948
|
-
...tail
|
|
949
|
-
];
|
|
950
|
-
}
|
|
1196
|
+
// src/cloud-foundry/ssh-one-shot.ts
|
|
951
1197
|
async function cfSshOneShot(appName, command, context, rawOptions = DEFAULT_CF_COMMAND_TIMEOUT_MS) {
|
|
952
1198
|
if (context.signal?.aborted || isDeadlineExpired(context)) {
|
|
953
1199
|
throw sshAbortError(context);
|
|
@@ -977,87 +1223,17 @@ function resolveSshOptions(raw, context) {
|
|
|
977
1223
|
maxOutputBytes
|
|
978
1224
|
};
|
|
979
1225
|
}
|
|
980
|
-
function
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
const next = incoming.byteLength <= remaining ? incoming : Buffer.from(incoming.subarray(0, remaining));
|
|
994
|
-
output.chunks.push(next);
|
|
995
|
-
output.bytes += next.byteLength;
|
|
996
|
-
}
|
|
997
|
-
function appendTail(output, data, outputLimit, retainedLimit) {
|
|
998
|
-
const incoming = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
999
|
-
if (incoming.byteLength === 0) {
|
|
1000
|
-
return;
|
|
1001
|
-
}
|
|
1002
|
-
output.chunks.push(incoming);
|
|
1003
|
-
output.bytes += incoming.byteLength;
|
|
1004
|
-
output.truncated ||= output.bytes > outputLimit;
|
|
1005
|
-
while (output.bytes > retainedLimit && output.chunks.length > 0) {
|
|
1006
|
-
const first = output.chunks[0];
|
|
1007
|
-
if (first === void 0) {
|
|
1008
|
-
break;
|
|
1009
|
-
}
|
|
1010
|
-
const excess = output.bytes - retainedLimit;
|
|
1011
|
-
if (first.byteLength <= excess) {
|
|
1012
|
-
output.chunks.shift();
|
|
1013
|
-
output.bytes -= first.byteLength;
|
|
1014
|
-
} else {
|
|
1015
|
-
output.chunks[0] = Buffer.from(first.subarray(excess));
|
|
1016
|
-
output.bytes -= excess;
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
function outputText(output) {
|
|
1021
|
-
return Buffer.concat(output.chunks, output.bytes).toString("utf8");
|
|
1022
|
-
}
|
|
1023
|
-
function createRedactionPolicy(values) {
|
|
1024
|
-
const sensitiveValues = normalizeSensitiveValues(values);
|
|
1025
|
-
const overlapBytes = sensitiveValues.reduce(
|
|
1026
|
-
(largest, value) => Math.max(largest, Buffer.byteLength(value)),
|
|
1027
|
-
0
|
|
1028
|
-
);
|
|
1029
|
-
return {
|
|
1030
|
-
safeToSurface: overlapBytes <= MAX_REDACTION_OVERLAP_BYTES && !sensitiveValues.some((value) => /[\r\n]/.test(value)),
|
|
1031
|
-
sensitiveValues,
|
|
1032
|
-
overlapBytes: Math.min(overlapBytes, MAX_REDACTION_OVERLAP_BYTES)
|
|
1033
|
-
};
|
|
1034
|
-
}
|
|
1035
|
-
function limitText(text, limit, keep) {
|
|
1036
|
-
const buffer = Buffer.from(text);
|
|
1037
|
-
if (buffer.byteLength <= limit) {
|
|
1038
|
-
return text;
|
|
1039
|
-
}
|
|
1040
|
-
const limited = keep === "head" ? buffer.subarray(0, limit) : buffer.subarray(buffer.byteLength - limit);
|
|
1041
|
-
return limited.toString("utf8");
|
|
1042
|
-
}
|
|
1043
|
-
function safeOutputText(output, policy, limit, keep) {
|
|
1044
|
-
if (!policy.safeToSurface && output.bytes > 0) {
|
|
1045
|
-
return SENSITIVE_OUTPUT_OMITTED;
|
|
1046
|
-
}
|
|
1047
|
-
const redacted = redactSensitiveText(outputText(output), policy.sensitiveValues);
|
|
1048
|
-
return limitText(redacted, limit, keep);
|
|
1049
|
-
}
|
|
1050
|
-
function createResult(exitCode, state) {
|
|
1051
|
-
const stdout = safeOutputText(state.stdout, state.redaction, state.outputLimit, "head");
|
|
1052
|
-
const stderr = safeOutputText(state.stderr, state.redaction, state.outputLimit, "head");
|
|
1053
|
-
return {
|
|
1054
|
-
exitCode,
|
|
1055
|
-
stdout,
|
|
1056
|
-
stderr,
|
|
1057
|
-
outputTruncated: state.stdout.truncated || state.stderr.truncated,
|
|
1058
|
-
stdoutTruncated: state.stdout.truncated,
|
|
1059
|
-
stderrTruncated: state.stderr.truncated
|
|
1060
|
-
};
|
|
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
|
+
};
|
|
1061
1237
|
}
|
|
1062
1238
|
function createSshExecutionState(context, outputLimit) {
|
|
1063
1239
|
const redaction = createRedactionPolicy(context.sensitiveValues ?? []);
|
|
@@ -1086,16 +1262,13 @@ async function captureSshChildIdentity(child) {
|
|
|
1086
1262
|
return;
|
|
1087
1263
|
}
|
|
1088
1264
|
}
|
|
1089
|
-
async function canSignalSshChild(child, childIdentity) {
|
|
1265
|
+
async function canSignalSshChild(child, childIdentity, signal) {
|
|
1090
1266
|
if (!childIsOpen(child) || child.pid === void 0) {
|
|
1091
1267
|
return false;
|
|
1092
1268
|
}
|
|
1093
|
-
if (nodeProcess3.platform === "win32") {
|
|
1094
|
-
return true;
|
|
1095
|
-
}
|
|
1096
1269
|
const expectedIdentity = await childIdentity;
|
|
1097
1270
|
if (expectedIdentity === void 0) {
|
|
1098
|
-
return
|
|
1271
|
+
return signal === "SIGTERM" && childIsOpen(child);
|
|
1099
1272
|
}
|
|
1100
1273
|
return await inspectProcessIdentity(child.pid, expectedIdentity) === "match" && childIsOpen(child);
|
|
1101
1274
|
}
|
|
@@ -1125,14 +1298,14 @@ function terminateSshExecution(child, childIdentity, state, settle) {
|
|
|
1125
1298
|
}
|
|
1126
1299
|
};
|
|
1127
1300
|
void (async () => {
|
|
1128
|
-
if (!await canSignalSshChild(child, childIdentity) || state.settled) {
|
|
1301
|
+
if (!await canSignalSshChild(child, childIdentity, "SIGTERM") || state.settled) {
|
|
1129
1302
|
settleUnverified();
|
|
1130
1303
|
return;
|
|
1131
1304
|
}
|
|
1132
1305
|
signalChild(child, "SIGTERM");
|
|
1133
1306
|
state.forceKillTimer = setTimeout(() => {
|
|
1134
1307
|
void (async () => {
|
|
1135
|
-
if (!await canSignalSshChild(child, childIdentity) || state.settled) {
|
|
1308
|
+
if (!await canSignalSshChild(child, childIdentity, "SIGKILL") || state.settled) {
|
|
1136
1309
|
settleUnverified();
|
|
1137
1310
|
return;
|
|
1138
1311
|
}
|
|
@@ -1141,18 +1314,6 @@ function terminateSshExecution(child, childIdentity, state, settle) {
|
|
|
1141
1314
|
}, 1e3);
|
|
1142
1315
|
})().catch(settleUnverified);
|
|
1143
1316
|
}
|
|
1144
|
-
function sshAbortError(context) {
|
|
1145
|
-
if (context.deadlineAt !== void 0 && Date.now() >= context.deadlineAt) {
|
|
1146
|
-
return new CfDebuggerError(
|
|
1147
|
-
"STARTUP_TIMEOUT",
|
|
1148
|
-
`Debugger startup exceeded its configured deadline during ${context.phase ?? "remote signalling"}.`
|
|
1149
|
-
);
|
|
1150
|
-
}
|
|
1151
|
-
return new CfDebuggerError("ABORTED", "Operation aborted by caller");
|
|
1152
|
-
}
|
|
1153
|
-
function isDeadlineExpired(context) {
|
|
1154
|
-
return context.deadlineAt !== void 0 && Date.now() >= context.deadlineAt;
|
|
1155
|
-
}
|
|
1156
1317
|
function createSshSettler(state, options, context, onAbort, resolve, reject) {
|
|
1157
1318
|
return (result) => {
|
|
1158
1319
|
if (state.settled) {
|
|
@@ -1227,6 +1388,9 @@ function signalChild(child, signal) {
|
|
|
1227
1388
|
} catch {
|
|
1228
1389
|
}
|
|
1229
1390
|
}
|
|
1391
|
+
|
|
1392
|
+
// src/cloud-foundry/ssh.ts
|
|
1393
|
+
var tunnelCaptures = /* @__PURE__ */ new WeakMap();
|
|
1230
1394
|
function isSshDisabledError(stderr) {
|
|
1231
1395
|
return stderr.toLowerCase().includes("ssh support is disabled");
|
|
1232
1396
|
}
|
|
@@ -1391,21 +1555,41 @@ function spawnSshTunnel(appName, localPort, remotePort, context, target = {}) {
|
|
|
1391
1555
|
}
|
|
1392
1556
|
const tunnelArg = `${localPort.toString()}:localhost:${remotePort.toString()}`;
|
|
1393
1557
|
const args = buildCfSshArgs(appName, target, ["-N", "-L", tunnelArg]);
|
|
1394
|
-
const child =
|
|
1558
|
+
const child = spawn2(resolveBin(context), [...args], {
|
|
1395
1559
|
env: buildEnv(context.cfHome),
|
|
1396
|
-
detached:
|
|
1560
|
+
detached: nodeProcess4.platform !== "win32",
|
|
1397
1561
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1398
1562
|
});
|
|
1399
1563
|
attachTunnelCapture(child, context);
|
|
1400
1564
|
return child;
|
|
1401
1565
|
}
|
|
1402
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
|
+
|
|
1403
1587
|
// src/session-state/store.ts
|
|
1404
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
1405
|
-
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";
|
|
1406
1590
|
import { hostname as getHostname2 } from "os";
|
|
1407
1591
|
import { dirname as dirname2, isAbsolute as isAbsolute3 } from "path";
|
|
1408
|
-
import
|
|
1592
|
+
import nodeProcess6 from "process";
|
|
1409
1593
|
|
|
1410
1594
|
// src/debug-session/constants.ts
|
|
1411
1595
|
var DEFAULT_TUNNEL_READY_TIMEOUT_MS = 18e4;
|
|
@@ -1504,18 +1688,7 @@ async function readLockOwner(lockPath) {
|
|
|
1504
1688
|
throw error;
|
|
1505
1689
|
}
|
|
1506
1690
|
}
|
|
1507
|
-
async function
|
|
1508
|
-
let modifiedAt2;
|
|
1509
|
-
try {
|
|
1510
|
-
modifiedAt2 = (await stat(lockPath)).mtimeMs;
|
|
1511
|
-
} catch (error) {
|
|
1512
|
-
if (errorCode2(error) === "ENOENT") {
|
|
1513
|
-
return true;
|
|
1514
|
-
}
|
|
1515
|
-
throw error;
|
|
1516
|
-
}
|
|
1517
|
-
const owner = await readLockOwner(lockPath);
|
|
1518
|
-
const ageMs = Date.now() - modifiedAt2;
|
|
1691
|
+
async function lockOwnerIsStale(owner, ageMs, staleMs) {
|
|
1519
1692
|
if (owner?.hostname === hostname()) {
|
|
1520
1693
|
if (!isProcessAlive(owner.pid)) {
|
|
1521
1694
|
return true;
|
|
@@ -1536,20 +1709,54 @@ async function isStaleLock(lockPath, staleMs) {
|
|
|
1536
1709
|
}
|
|
1537
1710
|
return ageMs > staleMs * FOREIGN_HOST_STALE_MULTIPLIER;
|
|
1538
1711
|
}
|
|
1539
|
-
async function
|
|
1540
|
-
|
|
1541
|
-
|
|
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;
|
|
1542
1727
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
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);
|
|
1547
1740
|
}
|
|
1548
|
-
|
|
1549
|
-
if (
|
|
1550
|
-
|
|
1741
|
+
try {
|
|
1742
|
+
if (lockFingerprint(await stat(lockPath)) !== observation.fingerprint) {
|
|
1743
|
+
return false;
|
|
1551
1744
|
}
|
|
1552
|
-
|
|
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);
|
|
1553
1760
|
}
|
|
1554
1761
|
async function reclaimStaleLock(lockPath, staleMs) {
|
|
1555
1762
|
const recoveryPath = `${lockPath}.recovery`;
|
|
@@ -1564,17 +1771,11 @@ async function reclaimStaleLock(lockPath, staleMs) {
|
|
|
1564
1771
|
throw error;
|
|
1565
1772
|
}
|
|
1566
1773
|
try {
|
|
1567
|
-
|
|
1774
|
+
const observation = await inspectLockStaleness(lockPath, staleMs);
|
|
1775
|
+
if (observation.status !== "present" || !observation.stale) {
|
|
1568
1776
|
return false;
|
|
1569
1777
|
}
|
|
1570
|
-
|
|
1571
|
-
await unlink(lockPath);
|
|
1572
|
-
} catch (error) {
|
|
1573
|
-
if (errorCode2(error) !== "ENOENT") {
|
|
1574
|
-
throw error;
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
|
-
return true;
|
|
1778
|
+
return await removeObservedLock(lockPath, observation);
|
|
1578
1779
|
} finally {
|
|
1579
1780
|
await releaseFileLock(recoveryPath, recoveryLock);
|
|
1580
1781
|
}
|
|
@@ -1582,12 +1783,13 @@ async function reclaimStaleLock(lockPath, staleMs) {
|
|
|
1582
1783
|
async function removeOwnedLock(lockPath, token) {
|
|
1583
1784
|
const owner = await readLockOwner(lockPath);
|
|
1584
1785
|
if (owner?.token !== token) {
|
|
1585
|
-
return;
|
|
1786
|
+
return false;
|
|
1586
1787
|
}
|
|
1587
|
-
await unlink(lockPath).catch((error) => {
|
|
1788
|
+
return await unlink(lockPath).then(() => true).catch((error) => {
|
|
1588
1789
|
if (errorCode2(error) !== "ENOENT") {
|
|
1589
1790
|
throw error;
|
|
1590
1791
|
}
|
|
1792
|
+
return false;
|
|
1591
1793
|
});
|
|
1592
1794
|
}
|
|
1593
1795
|
async function createFileLock(lockPath) {
|
|
@@ -1860,16 +2062,24 @@ function decodeSession(value) {
|
|
|
1860
2062
|
return void 0;
|
|
1861
2063
|
}
|
|
1862
2064
|
}
|
|
1863
|
-
function
|
|
2065
|
+
function decodeSessions(rawSessions) {
|
|
1864
2066
|
const seen = /* @__PURE__ */ new Set();
|
|
1865
|
-
const
|
|
1866
|
-
|
|
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
|
+
}
|
|
1867
2075
|
if (seen.has(session.sessionId)) {
|
|
1868
|
-
|
|
2076
|
+
dropped.push(`session[${index.toString()}]: duplicate sessionId ${session.sessionId}`);
|
|
2077
|
+
continue;
|
|
1869
2078
|
}
|
|
1870
2079
|
seen.add(session.sessionId);
|
|
2080
|
+
sessions.push(session);
|
|
1871
2081
|
}
|
|
1872
|
-
return
|
|
2082
|
+
return { dropped, sessions };
|
|
1873
2083
|
}
|
|
1874
2084
|
function decodeStateFileDetailed(value) {
|
|
1875
2085
|
if (typeof value !== "object" || value === null) {
|
|
@@ -1882,36 +2092,46 @@ function decodeStateFileDetailed(value) {
|
|
|
1882
2092
|
if (!Array.isArray(rawSessions)) {
|
|
1883
2093
|
return { kind: "invalid-file", reason: "sessions is not an array" };
|
|
1884
2094
|
}
|
|
1885
|
-
const decoded = rawSessions
|
|
1886
|
-
const valid = decoded.filter((session) => session !== void 0);
|
|
1887
|
-
const duplicates = duplicateSessionIds(valid);
|
|
1888
|
-
const sessions = valid.filter((session) => !duplicates.has(session.sessionId));
|
|
1889
|
-
const dropped = decoded.flatMap((session, index) => {
|
|
1890
|
-
if (session === void 0) {
|
|
1891
|
-
return [`session[${index.toString()}]: invalid entry`];
|
|
1892
|
-
}
|
|
1893
|
-
return duplicates.has(session.sessionId) ? [`session[${index.toString()}]: duplicate sessionId ${session.sessionId}`] : [];
|
|
1894
|
-
});
|
|
2095
|
+
const decoded = decodeSessions(rawSessions);
|
|
1895
2096
|
return {
|
|
1896
2097
|
kind: "decoded",
|
|
1897
|
-
state: { version: "2", sessions },
|
|
1898
|
-
dropped
|
|
2098
|
+
state: { version: "2", sessions: decoded.sessions },
|
|
2099
|
+
dropped: decoded.dropped
|
|
1899
2100
|
};
|
|
1900
2101
|
}
|
|
1901
2102
|
|
|
1902
2103
|
// src/session-state/health.ts
|
|
1903
2104
|
import { hostname as getHostname } from "os";
|
|
1904
|
-
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
|
+
}
|
|
1905
2124
|
|
|
1906
2125
|
// src/network/ports.ts
|
|
1907
2126
|
import { execFile as execFile3 } from "child_process";
|
|
1908
2127
|
import { readdir, readFile as readFile3, readlink } from "fs/promises";
|
|
1909
2128
|
import { get as httpGet } from "http";
|
|
1910
|
-
import {
|
|
2129
|
+
import { createServer, Socket } from "net";
|
|
1911
2130
|
import { promisify as promisify2 } from "util";
|
|
1912
2131
|
var execFileAsync2 = promisify2(execFile3);
|
|
1913
2132
|
var PROBE_INTERVAL_MS = 250;
|
|
1914
|
-
var
|
|
2133
|
+
var INITIAL_INSPECTOR_ATTEMPT_TIMEOUT_MS = 2500;
|
|
2134
|
+
var MAX_INSPECTOR_ATTEMPT_TIMEOUT_MS = 1e4;
|
|
1915
2135
|
var MAX_INSPECTOR_RESPONSE_BYTES = 64 * 1024;
|
|
1916
2136
|
var OWNER_COMMAND_TIMEOUT_MS = 5e3;
|
|
1917
2137
|
function sortedUniquePids(pids) {
|
|
@@ -2167,22 +2387,32 @@ async function isPortFree(port, signal) {
|
|
|
2167
2387
|
}
|
|
2168
2388
|
async function isPortListening(port, timeoutMs = 200) {
|
|
2169
2389
|
return await new Promise((resolve) => {
|
|
2170
|
-
const socket =
|
|
2171
|
-
|
|
2172
|
-
|
|
2390
|
+
const socket = new Socket();
|
|
2391
|
+
let settled = false;
|
|
2392
|
+
const finish = (listening) => {
|
|
2393
|
+
if (settled) {
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
settled = true;
|
|
2173
2397
|
socket.destroy();
|
|
2174
|
-
resolve(
|
|
2398
|
+
resolve(listening);
|
|
2399
|
+
};
|
|
2400
|
+
socket.once("connect", () => {
|
|
2401
|
+
finish(true);
|
|
2175
2402
|
});
|
|
2176
2403
|
socket.once("error", () => {
|
|
2177
|
-
|
|
2178
|
-
resolve(false);
|
|
2404
|
+
finish(false);
|
|
2179
2405
|
});
|
|
2180
2406
|
socket.once("timeout", () => {
|
|
2181
|
-
|
|
2182
|
-
resolve(false);
|
|
2407
|
+
finish(false);
|
|
2183
2408
|
});
|
|
2409
|
+
socket.setTimeout(positiveTimeout(timeoutMs));
|
|
2410
|
+
socket.connect({ port, host: "127.0.0.1" });
|
|
2184
2411
|
});
|
|
2185
2412
|
}
|
|
2413
|
+
function positiveTimeout(timeoutMs) {
|
|
2414
|
+
return Number.isFinite(timeoutMs) ? Math.max(1, Math.floor(timeoutMs)) : 1;
|
|
2415
|
+
}
|
|
2186
2416
|
function throwIfAborted2(signal) {
|
|
2187
2417
|
if (signal?.aborted) {
|
|
2188
2418
|
throw new CfDebuggerError("ABORTED", "Operation aborted by caller");
|
|
@@ -2210,7 +2440,7 @@ async function probeTunnelReady(port, timeoutMs, signal) {
|
|
|
2210
2440
|
throwIfAborted2(signal);
|
|
2211
2441
|
while (Date.now() < deadline) {
|
|
2212
2442
|
const remainingMs = deadline - Date.now();
|
|
2213
|
-
if (await isPortListening(port, Math.min(200, remainingMs))) {
|
|
2443
|
+
if (await isPortListening(port, positiveTimeout(Math.min(200, remainingMs)))) {
|
|
2214
2444
|
return true;
|
|
2215
2445
|
}
|
|
2216
2446
|
const waitMs = Math.min(PROBE_INTERVAL_MS, Math.max(0, deadline - Date.now()));
|
|
@@ -2279,21 +2509,28 @@ function readInspectorResponse(response, finish) {
|
|
|
2279
2509
|
finish(false);
|
|
2280
2510
|
});
|
|
2281
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
|
+
}
|
|
2282
2521
|
function probeInspectorAttempt(port, timeoutMs, signal) {
|
|
2283
2522
|
throwIfAborted2(signal);
|
|
2284
2523
|
return new Promise((resolve, reject) => {
|
|
2285
2524
|
const state = { settled: false };
|
|
2286
2525
|
const finish = (ready) => {
|
|
2287
|
-
|
|
2288
|
-
state.settled = true;
|
|
2289
|
-
clearTimeout(state.timer);
|
|
2290
|
-
state.removeAbortListener?.();
|
|
2291
|
-
resolve(ready);
|
|
2292
|
-
}
|
|
2526
|
+
finishInspectorAttempt(state, ready, resolve);
|
|
2293
2527
|
};
|
|
2294
|
-
const request = httpGet(
|
|
2295
|
-
|
|
2296
|
-
|
|
2528
|
+
const request = httpGet(
|
|
2529
|
+
{ agent: false, host: "127.0.0.1", port, path: "/json/list" },
|
|
2530
|
+
(response) => {
|
|
2531
|
+
readInspectorResponse(response, finish);
|
|
2532
|
+
}
|
|
2533
|
+
);
|
|
2297
2534
|
const onAbort = () => {
|
|
2298
2535
|
request.destroy();
|
|
2299
2536
|
if (!state.settled) {
|
|
@@ -2314,7 +2551,7 @@ function probeInspectorAttempt(port, timeoutMs, signal) {
|
|
|
2314
2551
|
state.timer = setTimeout(() => {
|
|
2315
2552
|
request.destroy();
|
|
2316
2553
|
finish(false);
|
|
2317
|
-
}, timeoutMs);
|
|
2554
|
+
}, positiveTimeout(timeoutMs));
|
|
2318
2555
|
request.once("error", () => {
|
|
2319
2556
|
if (signal?.aborted) {
|
|
2320
2557
|
onAbort();
|
|
@@ -2324,18 +2561,25 @@ function probeInspectorAttempt(port, timeoutMs, signal) {
|
|
|
2324
2561
|
});
|
|
2325
2562
|
});
|
|
2326
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
|
+
}
|
|
2327
2572
|
async function probeInspectorReady(port, timeoutMs, signal) {
|
|
2328
2573
|
const deadline = Date.now() + timeoutMs;
|
|
2574
|
+
let attempt = 0;
|
|
2329
2575
|
throwIfAborted2(signal);
|
|
2330
2576
|
while (Date.now() < deadline) {
|
|
2331
2577
|
const remainingMs = deadline - Date.now();
|
|
2332
|
-
const attemptTimeoutMs =
|
|
2333
|
-
1,
|
|
2334
|
-
Math.min(INSPECTOR_ATTEMPT_TIMEOUT_MS, remainingMs)
|
|
2335
|
-
);
|
|
2578
|
+
const attemptTimeoutMs = inspectorAttemptTimeout(attempt, remainingMs);
|
|
2336
2579
|
if (await probeInspectorAttempt(port, attemptTimeoutMs, signal)) {
|
|
2337
2580
|
return { status: "ready" };
|
|
2338
2581
|
}
|
|
2582
|
+
attempt += 1;
|
|
2339
2583
|
const waitMs = Math.min(PROBE_INTERVAL_MS, Math.max(0, deadline - Date.now()));
|
|
2340
2584
|
if (waitMs > 0) {
|
|
2341
2585
|
await waitForNextProbe(waitMs, signal);
|
|
@@ -2355,34 +2599,18 @@ function errorCode4(error) {
|
|
|
2355
2599
|
}
|
|
2356
2600
|
function isPidAlive(pid) {
|
|
2357
2601
|
try {
|
|
2358
|
-
|
|
2602
|
+
nodeProcess5.kill(pid, 0);
|
|
2359
2603
|
return true;
|
|
2360
2604
|
} catch (error) {
|
|
2361
2605
|
return errorCode4(error) !== "ESRCH";
|
|
2362
2606
|
}
|
|
2363
2607
|
}
|
|
2364
2608
|
function isProcessGroupAlive(pid) {
|
|
2365
|
-
return
|
|
2609
|
+
return nodeProcess5.platform !== "win32" && isPidAlive(-pid);
|
|
2366
2610
|
}
|
|
2367
2611
|
function isPidOrGroupAlive(pid) {
|
|
2368
2612
|
return isPidAlive(pid) || isProcessGroupAlive(pid);
|
|
2369
2613
|
}
|
|
2370
|
-
function startupAgeLimit(session) {
|
|
2371
|
-
return (session.startupTimeoutMs ?? MAX_STARTUP_TIMEOUT_MS) + STARTUP_STALE_SLACK_MS;
|
|
2372
|
-
}
|
|
2373
|
-
function startupExpired(session) {
|
|
2374
|
-
const startedAt = Date.parse(session.startedAt);
|
|
2375
|
-
return Number.isNaN(startedAt) || Date.now() - startedAt > startupAgeLimit(session);
|
|
2376
|
-
}
|
|
2377
|
-
async function inspectRecordedProcess(pid, identity, signal) {
|
|
2378
|
-
if (signal?.aborted) {
|
|
2379
|
-
throw new CfDebuggerError("ABORTED", "Session health inspection was aborted.");
|
|
2380
|
-
}
|
|
2381
|
-
if (!isPidAlive(pid)) {
|
|
2382
|
-
return "dead";
|
|
2383
|
-
}
|
|
2384
|
-
return await inspectProcessIdentity(pid, identity, signal);
|
|
2385
|
-
}
|
|
2386
2614
|
async function readySessionHealth(session, signal) {
|
|
2387
2615
|
const tunnelPid = session.tunnelPid;
|
|
2388
2616
|
if (tunnelPid === void 0) {
|
|
@@ -2391,6 +2619,7 @@ async function readySessionHealth(session, signal) {
|
|
|
2391
2619
|
const processVerdict = await inspectRecordedProcess(
|
|
2392
2620
|
tunnelPid,
|
|
2393
2621
|
session.tunnelProcessIdentity,
|
|
2622
|
+
isPidAlive,
|
|
2394
2623
|
signal
|
|
2395
2624
|
);
|
|
2396
2625
|
if (processVerdict === "dead" && isProcessGroupAlive(tunnelPid)) {
|
|
@@ -2428,6 +2657,7 @@ async function startingSessionHealth(session, signal) {
|
|
|
2428
2657
|
const processVerdict = await inspectRecordedProcess(
|
|
2429
2658
|
controllerPid,
|
|
2430
2659
|
session.controllerProcessIdentity,
|
|
2660
|
+
isPidAlive,
|
|
2431
2661
|
signal
|
|
2432
2662
|
);
|
|
2433
2663
|
if (processVerdict === "match") {
|
|
@@ -2447,19 +2677,16 @@ async function inspectSessionHealth(session, host = getHostname(), signal) {
|
|
|
2447
2677
|
}
|
|
2448
2678
|
return session.status === "ready" ? await readySessionHealth(session, signal) : await startingSessionHealth(session, signal);
|
|
2449
2679
|
}
|
|
2450
|
-
async function filterStaleSessions(sessions, signal) {
|
|
2451
|
-
const host = getHostname();
|
|
2452
|
-
const verdicts = await Promise.all(
|
|
2453
|
-
sessions.map(async (session) => [
|
|
2454
|
-
session,
|
|
2455
|
-
await inspectSessionHealth(session, host, signal)
|
|
2456
|
-
])
|
|
2457
|
-
);
|
|
2458
|
-
return verdicts.filter(([, verdict]) => verdict.status !== "stale").map(([session]) => session);
|
|
2459
|
-
}
|
|
2460
2680
|
|
|
2461
2681
|
// src/session-state/stop-intent.ts
|
|
2462
|
-
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";
|
|
2463
2690
|
function errorCode5(error) {
|
|
2464
2691
|
if (typeof error !== "object" || error === null) {
|
|
2465
2692
|
return void 0;
|
|
@@ -2494,6 +2721,29 @@ async function hasSessionStopIntent(sessionId) {
|
|
|
2494
2721
|
throw error;
|
|
2495
2722
|
}
|
|
2496
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
|
+
}
|
|
2497
2747
|
async function clearSessionStopIntent(sessionId) {
|
|
2498
2748
|
await unlink2(sessionStopIntentPath(sessionId)).catch((error) => {
|
|
2499
2749
|
if (errorCode5(error) !== "ENOENT") {
|
|
@@ -2515,7 +2765,7 @@ function errorCode6(error) {
|
|
|
2515
2765
|
async function readFileIfPresent(path) {
|
|
2516
2766
|
try {
|
|
2517
2767
|
await chmod3(path, 384);
|
|
2518
|
-
return await
|
|
2768
|
+
return await readFile5(path, "utf8");
|
|
2519
2769
|
} catch (error) {
|
|
2520
2770
|
if (errorCode6(error) === "ENOENT") {
|
|
2521
2771
|
return void 0;
|
|
@@ -2556,7 +2806,7 @@ async function preserveCorruptState(path, reason) {
|
|
|
2556
2806
|
const backup = corruptBackupPath(path);
|
|
2557
2807
|
await rename(path, backup);
|
|
2558
2808
|
await chmod3(backup, 384);
|
|
2559
|
-
|
|
2809
|
+
nodeProcess6.stderr.write(
|
|
2560
2810
|
`[cf-debugger] warning: preserved invalid state at ${backup} (${reason}).
|
|
2561
2811
|
`
|
|
2562
2812
|
);
|
|
@@ -2587,7 +2837,7 @@ async function readStateRaw() {
|
|
|
2587
2837
|
if (decoded.dropped.length > 0) {
|
|
2588
2838
|
await preserveCorruptState(path, decoded.dropped.join("; "));
|
|
2589
2839
|
await writeJsonFileAtomic(path, decoded.state);
|
|
2590
|
-
|
|
2840
|
+
nodeProcess6.stderr.write(
|
|
2591
2841
|
`[cf-debugger] warning: dropped ${decoded.dropped.length.toString()} invalid state entr${decoded.dropped.length === 1 ? "y" : "ies"}; valid sessions were retained.
|
|
2592
2842
|
`
|
|
2593
2843
|
);
|
|
@@ -2597,22 +2847,62 @@ async function readStateRaw() {
|
|
|
2597
2847
|
async function writeState(state) {
|
|
2598
2848
|
await writeJsonFileAtomic(stateFilePath(), state);
|
|
2599
2849
|
}
|
|
2600
|
-
|
|
2601
|
-
|
|
2850
|
+
function samePersistedSession(left, right) {
|
|
2851
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
2852
|
+
}
|
|
2853
|
+
async function inspectPruneCandidates(raw, signal) {
|
|
2602
2854
|
const host = getHostname2();
|
|
2603
|
-
const remote = raw.sessions.filter((session) => session.hostname !== host);
|
|
2604
2855
|
const local = raw.sessions.filter((session) => session.hostname === host);
|
|
2605
|
-
const
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
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
|
+
}
|
|
2609
2885
|
if (removed.length > 0) {
|
|
2610
|
-
|
|
2886
|
+
const remote = raw.sessions.filter((session) => session.hostname !== host);
|
|
2887
|
+
await writeState({ version: "2", sessions: [...remote, ...sessions] });
|
|
2611
2888
|
}
|
|
2612
|
-
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
|
+
);
|
|
2613
2903
|
}
|
|
2614
2904
|
async function readActiveSessions() {
|
|
2615
|
-
const result = await
|
|
2905
|
+
const result = await inspectAndPrune();
|
|
2616
2906
|
return result.sessions;
|
|
2617
2907
|
}
|
|
2618
2908
|
async function readSessionSnapshot(options) {
|
|
@@ -2621,12 +2911,7 @@ async function readSessionSnapshot(options) {
|
|
|
2621
2911
|
}, options);
|
|
2622
2912
|
}
|
|
2623
2913
|
async function readAndPruneActiveSessions(options) {
|
|
2624
|
-
|
|
2625
|
-
stateLockPath(),
|
|
2626
|
-
async () => await readAndPruneLocked(options?.signal),
|
|
2627
|
-
options
|
|
2628
|
-
);
|
|
2629
|
-
return { sessions: result.sessions, removed: result.removed };
|
|
2914
|
+
return await inspectAndPrune(options);
|
|
2630
2915
|
}
|
|
2631
2916
|
function sessionKeyString(key) {
|
|
2632
2917
|
const base = `${key.region}:${key.org}:${key.space}:${key.app}`;
|
|
@@ -2659,34 +2944,41 @@ function matchesRegistrationTarget(session, input, target) {
|
|
|
2659
2944
|
apiEndpoint: input.apiEndpoint
|
|
2660
2945
|
});
|
|
2661
2946
|
}
|
|
2662
|
-
|
|
2947
|
+
function portCandidates(preferred, range, scanOffset) {
|
|
2663
2948
|
const candidates = preferred === void 0 ? [] : [preferred];
|
|
2664
|
-
|
|
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;
|
|
2665
2952
|
if (port !== preferred) {
|
|
2666
2953
|
candidates.push(port);
|
|
2667
2954
|
}
|
|
2668
2955
|
}
|
|
2669
|
-
|
|
2956
|
+
return candidates;
|
|
2957
|
+
}
|
|
2958
|
+
async function pickPort(preferred, reserved, probe, range, scanOffset) {
|
|
2959
|
+
for (const port of portCandidates(preferred, range, scanOffset)) {
|
|
2670
2960
|
if (!reserved.has(port) && await probe(port)) {
|
|
2671
2961
|
return port;
|
|
2672
2962
|
}
|
|
2673
2963
|
}
|
|
2674
2964
|
throw new CfDebuggerError(
|
|
2675
2965
|
"PORT_UNAVAILABLE",
|
|
2676
|
-
`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()}`
|
|
2677
2967
|
);
|
|
2678
2968
|
}
|
|
2679
|
-
async function selectRegistrationCandidate(input, target, excluded) {
|
|
2969
|
+
async function selectRegistrationCandidate(input, target, excluded, range, scanOffset) {
|
|
2970
|
+
await readAndPruneActiveSessions(input.stateAccess);
|
|
2680
2971
|
return await withFileLock(stateLockPath(), async () => {
|
|
2681
|
-
const
|
|
2682
|
-
const
|
|
2972
|
+
const raw = await readStateRaw();
|
|
2973
|
+
const local = raw.sessions.filter((session) => session.hostname === getHostname2());
|
|
2974
|
+
const existing = local.find(
|
|
2683
2975
|
(session) => matchesRegistrationTarget(session, input, target)
|
|
2684
2976
|
);
|
|
2685
2977
|
if (existing !== void 0) {
|
|
2686
2978
|
return { existing };
|
|
2687
2979
|
}
|
|
2688
2980
|
const reserved = /* @__PURE__ */ new Set([
|
|
2689
|
-
...
|
|
2981
|
+
...local.map((session) => session.localPort),
|
|
2690
2982
|
...excluded
|
|
2691
2983
|
]);
|
|
2692
2984
|
return {
|
|
@@ -2694,20 +2986,58 @@ async function selectRegistrationCandidate(input, target, excluded) {
|
|
|
2694
2986
|
input.preferredPort,
|
|
2695
2987
|
reserved,
|
|
2696
2988
|
() => Promise.resolve(true),
|
|
2697
|
-
|
|
2698
|
-
|
|
2989
|
+
range,
|
|
2990
|
+
scanOffset
|
|
2699
2991
|
)
|
|
2700
2992
|
};
|
|
2701
2993
|
}, input.stateAccess);
|
|
2702
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
|
+
}
|
|
2703
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");
|
|
2704
3022
|
const remotePort = input.remotePort ?? DEFAULT_NODE_INSPECTOR_PORT;
|
|
2705
|
-
|
|
2706
|
-
throw new CfDebuggerError("UNSAFE_INPUT", "Remote inspector port must be from 1 to 65535.");
|
|
2707
|
-
}
|
|
3023
|
+
validateTcpPort(remotePort, "Remote inspector port");
|
|
2708
3024
|
if (input.startupTimeoutMs !== void 0 && (!Number.isSafeInteger(input.startupTimeoutMs) || input.startupTimeoutMs <= 0 || input.startupTimeoutMs > MAX_STARTUP_TIMEOUT_MS)) {
|
|
2709
3025
|
throw new CfDebuggerError("UNSAFE_INPUT", "Persisted startup timeout is outside the supported range.");
|
|
2710
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);
|
|
2711
3041
|
}
|
|
2712
3042
|
function createRegisteredSession(input, target, localPort, controllerProcessIdentity) {
|
|
2713
3043
|
const sessionId = (input.sessionIdFactory ?? randomUUID2)();
|
|
@@ -2720,8 +3050,8 @@ function createRegisteredSession(input, target, localPort, controllerProcessIden
|
|
|
2720
3050
|
}
|
|
2721
3051
|
return {
|
|
2722
3052
|
sessionId,
|
|
2723
|
-
pid:
|
|
2724
|
-
controllerPid:
|
|
3053
|
+
pid: nodeProcess6.pid,
|
|
3054
|
+
controllerPid: nodeProcess6.pid,
|
|
2725
3055
|
...controllerProcessIdentity === void 0 ? {} : { controllerProcessIdentity },
|
|
2726
3056
|
hostname: getHostname2(),
|
|
2727
3057
|
region: input.region,
|
|
@@ -2741,37 +3071,46 @@ function createRegisteredSession(input, target, localPort, controllerProcessIden
|
|
|
2741
3071
|
};
|
|
2742
3072
|
}
|
|
2743
3073
|
async function persistRegistration(input, target, candidate, controllerProcessIdentity) {
|
|
3074
|
+
await readAndPruneActiveSessions(input.stateAccess);
|
|
2744
3075
|
return await withFileLock(stateLockPath(), async () => {
|
|
2745
|
-
const
|
|
2746
|
-
const
|
|
3076
|
+
const raw = await readStateRaw();
|
|
3077
|
+
const local = raw.sessions.filter((session2) => session2.hostname === getHostname2());
|
|
3078
|
+
const existing = local.find(
|
|
2747
3079
|
(session2) => matchesRegistrationTarget(session2, input, target)
|
|
2748
3080
|
);
|
|
2749
3081
|
if (existing !== void 0) {
|
|
2750
3082
|
return { session: existing, existing };
|
|
2751
3083
|
}
|
|
2752
|
-
const reserved =
|
|
3084
|
+
const reserved = local.some((session2) => session2.localPort === candidate);
|
|
2753
3085
|
if (reserved || !await input.portProbe(candidate)) {
|
|
2754
3086
|
return void 0;
|
|
2755
3087
|
}
|
|
2756
3088
|
const session = createRegisteredSession(input, target, candidate, controllerProcessIdentity);
|
|
2757
3089
|
await writeState({
|
|
2758
3090
|
version: "2",
|
|
2759
|
-
sessions: [...
|
|
3091
|
+
sessions: [...raw.sessions, session]
|
|
2760
3092
|
});
|
|
2761
3093
|
return { session };
|
|
2762
3094
|
}, input.stateAccess);
|
|
2763
3095
|
}
|
|
2764
3096
|
async function registerNewSession(input) {
|
|
2765
|
-
validateSessionInput(input);
|
|
3097
|
+
const range = validateSessionInput(input);
|
|
2766
3098
|
const target = resolveNodeTarget(input);
|
|
3099
|
+
const scanOffset = registrationScanOffset(input, target, range);
|
|
2767
3100
|
const controllerIdentity = await readProcessIdentity(
|
|
2768
|
-
|
|
3101
|
+
nodeProcess6.pid,
|
|
2769
3102
|
input.stateAccess?.signal
|
|
2770
3103
|
);
|
|
2771
3104
|
const excluded = /* @__PURE__ */ new Set();
|
|
2772
|
-
const maximumAttempts =
|
|
3105
|
+
const maximumAttempts = range.maxPort - range.basePort + 2;
|
|
2773
3106
|
for (let attempt = 0; attempt < maximumAttempts; attempt += 1) {
|
|
2774
|
-
const selection = await selectRegistrationCandidate(
|
|
3107
|
+
const selection = await selectRegistrationCandidate(
|
|
3108
|
+
input,
|
|
3109
|
+
target,
|
|
3110
|
+
excluded,
|
|
3111
|
+
range,
|
|
3112
|
+
scanOffset
|
|
3113
|
+
);
|
|
2775
3114
|
if (selection.existing !== void 0) {
|
|
2776
3115
|
return { session: selection.existing, existing: selection.existing };
|
|
2777
3116
|
}
|
|
@@ -2885,9 +3224,6 @@ async function requestSessionStop(sessionId) {
|
|
|
2885
3224
|
if (target === void 0) {
|
|
2886
3225
|
return void 0;
|
|
2887
3226
|
}
|
|
2888
|
-
if (target.status === "ready") {
|
|
2889
|
-
return { session: target, previousStatus: target.status };
|
|
2890
|
-
}
|
|
2891
3227
|
await writeSessionStopIntent(sessionId);
|
|
2892
3228
|
if (target.stopRequestedAt !== void 0) {
|
|
2893
3229
|
return { session: target, previousStatus: target.status };
|
|
@@ -2941,51 +3277,59 @@ function cliErrorExitCode(error) {
|
|
|
2941
3277
|
}
|
|
2942
3278
|
return hasCleanupFailure(error) ? CLEANUP_FAILURE_EXIT_CODE : 1;
|
|
2943
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
|
+
}
|
|
2944
3286
|
|
|
2945
3287
|
// src/debug-session/lifecycle.ts
|
|
2946
3288
|
import { constants as osConstants } from "os";
|
|
2947
3289
|
|
|
2948
3290
|
// src/debug-session/processes.ts
|
|
2949
3291
|
import process3 from "process";
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
return "terminated";
|
|
2955
|
-
}
|
|
2956
|
-
if (verifyBeforeSignal !== void 0 && !await verifyBeforeSignal("SIGTERM")) {
|
|
2957
|
-
return "ownership-lost";
|
|
2958
|
-
}
|
|
3292
|
+
function terminationTargetAlive(pid, targetKind) {
|
|
3293
|
+
return targetKind === "group" ? isProcessGroupAlive(pid) : isPidAlive(pid);
|
|
3294
|
+
}
|
|
3295
|
+
function signalTerminationTarget(pid, targetKind, signal) {
|
|
2959
3296
|
try {
|
|
2960
|
-
process3.kill(targetKind === "group" ? -pid : pid,
|
|
3297
|
+
process3.kill(targetKind === "group" ? -pid : pid, signal);
|
|
2961
3298
|
} catch {
|
|
2962
3299
|
}
|
|
3300
|
+
}
|
|
3301
|
+
async function waitForTargetExit(pid, targetKind, timeoutMs) {
|
|
2963
3302
|
const startedAt = Date.now();
|
|
2964
3303
|
while (Date.now() - startedAt < timeoutMs) {
|
|
2965
|
-
if (!
|
|
2966
|
-
return
|
|
3304
|
+
if (!terminationTargetAlive(pid, targetKind)) {
|
|
3305
|
+
return true;
|
|
2967
3306
|
}
|
|
2968
3307
|
await new Promise((resolve) => {
|
|
2969
3308
|
setTimeout(resolve, PID_TERMINATION_POLL_MS);
|
|
2970
3309
|
});
|
|
2971
3310
|
}
|
|
2972
|
-
|
|
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")) {
|
|
2973
3322
|
return "ownership-lost";
|
|
2974
3323
|
}
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
3324
|
+
signalTerminationTarget(pid, targetKind, "SIGTERM");
|
|
3325
|
+
if (await waitForTargetExit(pid, targetKind, timeoutMs)) {
|
|
3326
|
+
return "terminated";
|
|
2978
3327
|
}
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
if (!targetAlive()) {
|
|
2982
|
-
return "terminated";
|
|
2983
|
-
}
|
|
2984
|
-
await new Promise((resolve) => {
|
|
2985
|
-
setTimeout(resolve, PID_TERMINATION_POLL_MS);
|
|
2986
|
-
});
|
|
3328
|
+
if (!await signalIsAuthorized(verifyBeforeSignal, "SIGKILL")) {
|
|
3329
|
+
return "ownership-lost";
|
|
2987
3330
|
}
|
|
2988
|
-
|
|
3331
|
+
signalTerminationTarget(pid, targetKind, "SIGKILL");
|
|
3332
|
+
return await waitForTargetExit(pid, targetKind, CHILD_SIGKILL_GRACE_MS) ? "terminated" : "still-alive";
|
|
2989
3333
|
}
|
|
2990
3334
|
async function killProcessGroupOrProc(child, verifyBeforeSignal, timeoutMs = CHILD_SIGTERM_GRACE_MS) {
|
|
2991
3335
|
if (child.pid === void 0) {
|
|
@@ -3004,7 +3348,39 @@ async function killProcessGroupOrProc(child, verifyBeforeSignal, timeoutMs = CHI
|
|
|
3004
3348
|
}
|
|
3005
3349
|
|
|
3006
3350
|
// src/debug-session/session-home.ts
|
|
3007
|
-
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
|
+
}
|
|
3008
3384
|
async function removeOwnedSessionCfHome(sessionId, candidate) {
|
|
3009
3385
|
if (!isOwnedSessionCfHomeDir(sessionId, candidate)) {
|
|
3010
3386
|
throw new CfDebuggerError(
|
|
@@ -3012,6 +3388,10 @@ async function removeOwnedSessionCfHome(sessionId, candidate) {
|
|
|
3012
3388
|
`Refusing to remove unowned debugger CF home for session ${sessionId}.`
|
|
3013
3389
|
);
|
|
3014
3390
|
}
|
|
3391
|
+
const rootInspection = await inspectSessionHomesRoot(dirname3(candidate));
|
|
3392
|
+
if (rootInspection.status === "unsafe") {
|
|
3393
|
+
throw new CfDebuggerError("UNSAFE_INPUT", rootInspection.reason);
|
|
3394
|
+
}
|
|
3015
3395
|
await rm(candidate, { recursive: true, force: true });
|
|
3016
3396
|
}
|
|
3017
3397
|
async function tryRemoveOwnedSessionCfHome(sessionId, candidate) {
|
|
@@ -3026,13 +3406,30 @@ async function tryRemoveOwnedSessionCfHome(sessionId, candidate) {
|
|
|
3026
3406
|
}
|
|
3027
3407
|
}
|
|
3028
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
|
+
|
|
3029
3425
|
// src/debug-session/lifecycle.ts
|
|
3030
3426
|
var handleLifecycles = /* @__PURE__ */ new WeakMap();
|
|
3031
3427
|
function childIsOpen2(child) {
|
|
3032
3428
|
return child.exitCode === null && child.signalCode === null;
|
|
3033
3429
|
}
|
|
3034
3430
|
function signalExitCode(signal) {
|
|
3035
|
-
|
|
3431
|
+
const signum = osConstants.signals[signal];
|
|
3432
|
+
return typeof signum === "number" && Number.isFinite(signum) ? 128 + signum : 1;
|
|
3036
3433
|
}
|
|
3037
3434
|
function unexpectedExitCode(code, signal) {
|
|
3038
3435
|
if (signal !== null) {
|
|
@@ -3046,6 +3443,8 @@ function emitSafely(emit, status, message) {
|
|
|
3046
3443
|
} catch {
|
|
3047
3444
|
}
|
|
3048
3445
|
}
|
|
3446
|
+
function ignoreCleanupFailure() {
|
|
3447
|
+
}
|
|
3049
3448
|
async function runCleanupActions(actions, aggregateMessage) {
|
|
3050
3449
|
const errors = [];
|
|
3051
3450
|
for (const action of actions) {
|
|
@@ -3063,10 +3462,10 @@ async function runCleanupActions(actions, aggregateMessage) {
|
|
|
3063
3462
|
}
|
|
3064
3463
|
}
|
|
3065
3464
|
async function handleTunnelClose(sessionId, code, signal, child, resolveExit, isFinalizing, hasFailed, markFailed, emit) {
|
|
3066
|
-
const stopRequested = await
|
|
3465
|
+
const stopRequested = isFinalizing() ? false : await hasExpectedStopSignal(sessionId);
|
|
3067
3466
|
const expected = isFinalizing() || stopRequested;
|
|
3068
3467
|
if (stopRequested) {
|
|
3069
|
-
await clearSessionStopIntent(sessionId);
|
|
3468
|
+
await clearSessionStopIntent(sessionId).catch(ignoreCleanupFailure);
|
|
3070
3469
|
}
|
|
3071
3470
|
if (!expected && !hasFailed()) {
|
|
3072
3471
|
const detail = signal === null ? code === null ? "no exit status" : `exit code ${code.toString()}` : `signal ${signal}`;
|
|
@@ -3081,6 +3480,19 @@ async function handleTunnelClose(sessionId, code, signal, child, resolveExit, is
|
|
|
3081
3480
|
}
|
|
3082
3481
|
resolveExit(expected && !hasFailed() ? 0 : unexpectedExitCode(code, signal));
|
|
3083
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
|
+
}
|
|
3084
3496
|
function attachTunnelEvents(sessionId, child, resolveExit, isFinalizing, hasFailed, markFailed, emit) {
|
|
3085
3497
|
child.once("close", (code, signal) => {
|
|
3086
3498
|
void handleTunnelClose(
|
|
@@ -3123,87 +3535,126 @@ async function expectedTunnelStillOwned(session, child) {
|
|
|
3123
3535
|
return ownership.status === "unverified" ? "unverified" : ownership.status === "owned";
|
|
3124
3536
|
}
|
|
3125
3537
|
async function cleanupFinishedSession(session) {
|
|
3126
|
-
await
|
|
3127
|
-
await
|
|
3128
|
-
|
|
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
|
+
}
|
|
3129
3546
|
}
|
|
3130
|
-
function
|
|
3131
|
-
let child;
|
|
3132
|
-
let childIdentity;
|
|
3133
|
-
let childFailed = false;
|
|
3134
|
-
let tunnelError;
|
|
3135
|
-
let finalizing = false;
|
|
3547
|
+
function initializeTunnelLifecycle() {
|
|
3136
3548
|
let exitResolve = (_code) => {
|
|
3137
3549
|
throw new Error("Tunnel exit resolver was used before initialization.");
|
|
3138
3550
|
};
|
|
3139
3551
|
const exitPromise = new Promise((resolve) => {
|
|
3140
3552
|
exitResolve = resolve;
|
|
3141
3553
|
});
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
(error) => {
|
|
3152
|
-
if (!childFailed) {
|
|
3153
|
-
childFailed = true;
|
|
3154
|
-
tunnelError = error;
|
|
3155
|
-
}
|
|
3156
|
-
},
|
|
3157
|
-
emit
|
|
3158
|
-
);
|
|
3159
|
-
};
|
|
3160
|
-
const verifyBeforeSignal = async (signal) => {
|
|
3161
|
-
const tunnelChild = child;
|
|
3162
|
-
const childPid = tunnelChild?.pid;
|
|
3163
|
-
if (tunnelChild === void 0 || childPid === void 0 || !childIsOpen2(tunnelChild)) {
|
|
3164
|
-
return false;
|
|
3165
|
-
}
|
|
3166
|
-
const expectedIdentity = await childIdentity;
|
|
3167
|
-
if (expectedIdentity !== void 0) {
|
|
3168
|
-
return await inspectProcessIdentity(childPid, expectedIdentity) === "match" && childIsOpen2(tunnelChild);
|
|
3169
|
-
}
|
|
3170
|
-
if (signal === "SIGKILL") {
|
|
3171
|
-
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
|
|
3172
3563
|
}
|
|
3173
|
-
return true;
|
|
3174
3564
|
};
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
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);
|
|
3191
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;
|
|
3192
3645
|
return {
|
|
3193
|
-
exitPromise,
|
|
3646
|
+
exitPromise: initialized.exitPromise,
|
|
3194
3647
|
assertRunning: () => {
|
|
3195
|
-
|
|
3196
|
-
throw new CfDebuggerError(
|
|
3197
|
-
"TUNNEL_NOT_READY",
|
|
3198
|
-
`SSH tunnel for session ${session.sessionId} exited before readiness could be committed.`,
|
|
3199
|
-
child === void 0 ? void 0 : formatTunnelDiagnostics(getTunnelDiagnostics(child))
|
|
3200
|
-
);
|
|
3201
|
-
}
|
|
3648
|
+
assertTunnelRunning(state, session);
|
|
3202
3649
|
},
|
|
3203
|
-
finalize
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3650
|
+
finalize: async (emitStopped) => {
|
|
3651
|
+
await finalizeTunnelLifecycle(state, session, emit, emitStopped);
|
|
3652
|
+
},
|
|
3653
|
+
observeChild: (child) => {
|
|
3654
|
+
observeTunnelChild(state, session.sessionId, child, emit);
|
|
3655
|
+
},
|
|
3656
|
+
failed: () => state.childFailed,
|
|
3657
|
+
error: () => state.tunnelError
|
|
3207
3658
|
};
|
|
3208
3659
|
}
|
|
3209
3660
|
function createDebuggerHandle(session, emit, lifecycle) {
|
|
@@ -3311,7 +3762,7 @@ function throwIfStartupAborted(signal, expiresAt, timeoutMs, phase) {
|
|
|
3311
3762
|
|
|
3312
3763
|
// src/debug-session/start.ts
|
|
3313
3764
|
import { chmod as chmod4, mkdir as mkdir4 } from "fs/promises";
|
|
3314
|
-
import
|
|
3765
|
+
import nodeProcess7 from "process";
|
|
3315
3766
|
|
|
3316
3767
|
// src/debug-session/orphans.ts
|
|
3317
3768
|
import { readdir as readdir2, stat as stat2, unlink as unlink4 } from "fs/promises";
|
|
@@ -3354,6 +3805,16 @@ async function pruneAndCleanupOrphans(stateAccess) {
|
|
|
3354
3805
|
|
|
3355
3806
|
// src/debug-session/startup-cancellation.ts
|
|
3356
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
|
+
}
|
|
3357
3818
|
function createStartupCancellation(sessionId, callerSignal) {
|
|
3358
3819
|
const controller = new AbortController();
|
|
3359
3820
|
let active = true;
|
|
@@ -3369,7 +3830,7 @@ function createStartupCancellation(sessionId, callerSignal) {
|
|
|
3369
3830
|
};
|
|
3370
3831
|
const poll = async () => {
|
|
3371
3832
|
try {
|
|
3372
|
-
if (active && await
|
|
3833
|
+
if (active && await startupStopWasRequested(sessionId)) {
|
|
3373
3834
|
controller.abort();
|
|
3374
3835
|
}
|
|
3375
3836
|
} catch {
|
|
@@ -3553,6 +4014,8 @@ async function signalRemoteNode(inputs) {
|
|
|
3553
4014
|
}
|
|
3554
4015
|
|
|
3555
4016
|
// src/debug-session/startup-tunnel.ts
|
|
4017
|
+
var INSPECTOR_READY_RESERVE_MS = 1e4;
|
|
4018
|
+
var OWNER_READY_ATTEMPT_MAX_MS = 5e3;
|
|
3556
4019
|
function linkAbortSignals(signals) {
|
|
3557
4020
|
const controller = new AbortController();
|
|
3558
4021
|
const subscriptions = [];
|
|
@@ -3576,6 +4039,35 @@ function linkAbortSignals(signals) {
|
|
|
3576
4039
|
}
|
|
3577
4040
|
};
|
|
3578
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
|
+
}
|
|
3579
4071
|
function diagnosticsStderr(child) {
|
|
3580
4072
|
return formatTunnelDiagnostics(getTunnelDiagnostics(child));
|
|
3581
4073
|
}
|
|
@@ -3596,6 +4088,33 @@ function remainingReadyTimeout(inputs) {
|
|
|
3596
4088
|
Math.min(inputs.tunnelReadyTimeoutMs, inputs.context.deadlineAt - Date.now())
|
|
3597
4089
|
);
|
|
3598
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
|
+
}
|
|
3599
4118
|
function startupStateAccess(inputs) {
|
|
3600
4119
|
return {
|
|
3601
4120
|
...inputs.context.signal === void 0 ? {} : { signal: inputs.context.signal },
|
|
@@ -3620,6 +4139,7 @@ function ownerVerificationError(localPort, inspection, child) {
|
|
|
3620
4139
|
}
|
|
3621
4140
|
async function waitForLocalTunnel(inputs, child, timeoutMs) {
|
|
3622
4141
|
const childEnded = new AbortController();
|
|
4142
|
+
const childExit = observeChildExit(child);
|
|
3623
4143
|
const linked = linkAbortSignals([
|
|
3624
4144
|
...inputs.context.signal === void 0 ? [] : [inputs.context.signal],
|
|
3625
4145
|
childEnded.signal
|
|
@@ -3628,7 +4148,7 @@ async function waitForLocalTunnel(inputs, child, timeoutMs) {
|
|
|
3628
4148
|
const probe = probeTunnelReady(inputs.session.localPort, timeoutMs, linked.signal);
|
|
3629
4149
|
const winner = await Promise.race([
|
|
3630
4150
|
probe.then((ready) => ({ kind: "probe", ready })),
|
|
3631
|
-
|
|
4151
|
+
childExit.promise.then(() => ({ kind: "child-exit" }))
|
|
3632
4152
|
]);
|
|
3633
4153
|
if (winner.kind === "probe" && winner.ready) {
|
|
3634
4154
|
return;
|
|
@@ -3638,6 +4158,7 @@ async function waitForLocalTunnel(inputs, child, timeoutMs) {
|
|
|
3638
4158
|
await probe.catch(() => false);
|
|
3639
4159
|
}
|
|
3640
4160
|
} finally {
|
|
4161
|
+
childExit.dispose();
|
|
3641
4162
|
linked.dispose();
|
|
3642
4163
|
}
|
|
3643
4164
|
throw new CfDebuggerError(
|
|
@@ -3646,28 +4167,38 @@ async function waitForLocalTunnel(inputs, child, timeoutMs) {
|
|
|
3646
4167
|
diagnosticsStderr(child)
|
|
3647
4168
|
);
|
|
3648
4169
|
}
|
|
3649
|
-
async function verifyLocalOwner(inputs, child, childPid) {
|
|
3650
|
-
const
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
);
|
|
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
|
+
}
|
|
4192
|
+
} finally {
|
|
4193
|
+
linked.dispose();
|
|
4194
|
+
}
|
|
3655
4195
|
if (ownership.status !== "owned") {
|
|
3656
4196
|
throw ownerVerificationError(inputs.session.localPort, ownership, child);
|
|
3657
4197
|
}
|
|
3658
4198
|
}
|
|
3659
|
-
function childExitPromise(child) {
|
|
3660
|
-
if (child.exitCode !== null || child.signalCode !== null) {
|
|
3661
|
-
return Promise.resolve();
|
|
3662
|
-
}
|
|
3663
|
-
return new Promise((resolve) => {
|
|
3664
|
-
child.once("close", () => {
|
|
3665
|
-
resolve();
|
|
3666
|
-
});
|
|
3667
|
-
});
|
|
3668
|
-
}
|
|
3669
4199
|
async function verifyInspector(inputs, child, timeoutMs) {
|
|
3670
4200
|
const childEnded = new AbortController();
|
|
4201
|
+
const childExit = observeChildExit(child);
|
|
3671
4202
|
const linked = linkAbortSignals([
|
|
3672
4203
|
...inputs.context.signal === void 0 ? [] : [inputs.context.signal],
|
|
3673
4204
|
childEnded.signal
|
|
@@ -3676,7 +4207,7 @@ async function verifyInspector(inputs, child, timeoutMs) {
|
|
|
3676
4207
|
const probe = probeInspectorReady(inputs.session.localPort, timeoutMs, linked.signal);
|
|
3677
4208
|
const winner = await Promise.race([
|
|
3678
4209
|
probe.then((result) => ({ kind: "probe", result })),
|
|
3679
|
-
|
|
4210
|
+
childExit.promise.then(() => ({ kind: "child-exit" }))
|
|
3680
4211
|
]);
|
|
3681
4212
|
if (winner.kind === "probe" && winner.result.status === "ready") {
|
|
3682
4213
|
return;
|
|
@@ -3686,6 +4217,7 @@ async function verifyInspector(inputs, child, timeoutMs) {
|
|
|
3686
4217
|
await probe.catch(() => ({ status: "unreachable" }));
|
|
3687
4218
|
}
|
|
3688
4219
|
} finally {
|
|
4220
|
+
childExit.dispose();
|
|
3689
4221
|
linked.dispose();
|
|
3690
4222
|
}
|
|
3691
4223
|
throw new CfDebuggerError(
|
|
@@ -3743,15 +4275,14 @@ async function openReadyTunnel(inputs) {
|
|
|
3743
4275
|
inputs.onChild(child);
|
|
3744
4276
|
const childPid = requireChildPid(child);
|
|
3745
4277
|
await recordTunnelPid(inputs, childPid);
|
|
3746
|
-
const
|
|
3747
|
-
|
|
3748
|
-
await waitForLocalTunnel(inputs, child, timeoutMs);
|
|
4278
|
+
const readyWindow = createReadyWindow(remainingReadyTimeout(inputs));
|
|
4279
|
+
await waitForLocalTunnel(inputs, child, readyWindow.localTimeoutMs);
|
|
3749
4280
|
ensureStartupActive(inputs, "local tunnel binding");
|
|
3750
|
-
await verifyLocalOwner(inputs, child, childPid);
|
|
4281
|
+
await verifyLocalOwner(inputs, child, childPid, readyWindow.ownerTimeoutMs);
|
|
3751
4282
|
ensureStartupActive(inputs, "local tunnel ownership verification");
|
|
3752
|
-
await verifyInspector(inputs, child,
|
|
4283
|
+
await verifyInspector(inputs, child, remainingInspectorMs(readyWindow));
|
|
3753
4284
|
ensureStartupActive(inputs, "inspector readiness verification");
|
|
3754
|
-
await verifyLocalOwner(inputs, child, childPid);
|
|
4285
|
+
await verifyLocalOwner(inputs, child, childPid, remainingWindowMs(readyWindow));
|
|
3755
4286
|
ensureStartupActive(inputs, "final local tunnel ownership verification");
|
|
3756
4287
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
3757
4288
|
throw new CfDebuggerError(
|
|
@@ -3763,9 +4294,17 @@ async function openReadyTunnel(inputs) {
|
|
|
3763
4294
|
}
|
|
3764
4295
|
|
|
3765
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
|
+
}
|
|
3766
4305
|
function requireCredentials(options) {
|
|
3767
|
-
const email = options.email ??
|
|
3768
|
-
const password = options.password ??
|
|
4306
|
+
const email = options.email ?? nodeProcess7.env["SAP_EMAIL"];
|
|
4307
|
+
const password = options.password ?? nodeProcess7.env["SAP_PASSWORD"];
|
|
3769
4308
|
if (email === void 0 || email.length === 0) {
|
|
3770
4309
|
throw new CfDebuggerError(
|
|
3771
4310
|
"MISSING_CREDENTIALS",
|
|
@@ -3951,11 +4490,11 @@ async function establishDebuggerSession(inputs) {
|
|
|
3951
4490
|
return readySession;
|
|
3952
4491
|
}
|
|
3953
4492
|
function writeWarning(message) {
|
|
3954
|
-
|
|
4493
|
+
nodeProcess7.stderr.write(`[cf-debugger] warning: ${message}
|
|
3955
4494
|
`);
|
|
3956
4495
|
}
|
|
3957
4496
|
function writeTunnelOutput(stream, text) {
|
|
3958
|
-
|
|
4497
|
+
nodeProcess7.stderr.write(`[cf-debugger tunnel ${stream}] ${text}
|
|
3959
4498
|
`);
|
|
3960
4499
|
}
|
|
3961
4500
|
function retryMessage(status) {
|
|
@@ -3997,59 +4536,102 @@ function normalizeStartupError(error, expiresAt, timeoutMs) {
|
|
|
3997
4536
|
}
|
|
3998
4537
|
return error;
|
|
3999
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
|
+
}
|
|
4000
4627
|
async function startDebuggerUsingDeadline(options, deadline) {
|
|
4001
|
-
const
|
|
4002
|
-
const
|
|
4003
|
-
|
|
4004
|
-
const startupTimeoutMs = deadline.timeoutMs;
|
|
4005
|
-
const tunnelReadyTimeoutMs = resolveTunnelReadyTimeoutMs(options.tunnelReadyTimeoutMs);
|
|
4006
|
-
const tracker = createStatusTracker(options);
|
|
4007
|
-
tracker.emit("starting");
|
|
4008
|
-
let cancellation;
|
|
4009
|
-
let lifecycle;
|
|
4628
|
+
const plan = resolveStartupPlan(options);
|
|
4629
|
+
const resources = {};
|
|
4630
|
+
plan.tracker.emit("starting");
|
|
4010
4631
|
try {
|
|
4011
|
-
|
|
4012
|
-
await pruneAndCleanupOrphans(
|
|
4013
|
-
startupStateAccess2(deadline.signal, deadline.expiresAt)
|
|
4014
|
-
);
|
|
4015
|
-
throwIfStartupAborted(deadline.signal, deadline.expiresAt, startupTimeoutMs, "state cleanup");
|
|
4016
|
-
const session = await registerSession(options, target, apiEndpoint, deadline);
|
|
4017
|
-
cancellation = createStartupCancellation(session.sessionId, deadline.signal);
|
|
4018
|
-
const context = createCfContext(
|
|
4019
|
-
session,
|
|
4020
|
-
credentials,
|
|
4021
|
-
cancellation,
|
|
4022
|
-
deadline,
|
|
4023
|
-
tracker,
|
|
4024
|
-
options.verbose === true
|
|
4025
|
-
);
|
|
4026
|
-
lifecycle = createTunnelLifecycle(session, tracker.emit);
|
|
4027
|
-
const activeSession = await establishDebuggerSession({
|
|
4028
|
-
options: { ...options, remotePort: session.remotePort },
|
|
4029
|
-
target,
|
|
4030
|
-
session,
|
|
4031
|
-
context,
|
|
4032
|
-
credentials,
|
|
4033
|
-
tunnelReadyTimeoutMs,
|
|
4034
|
-
emit: tracker.emit,
|
|
4035
|
-
transition: createTransition(session.sessionId, context),
|
|
4036
|
-
lifecycle
|
|
4037
|
-
});
|
|
4038
|
-
cancellation.dispose();
|
|
4039
|
-
deadline.dispose();
|
|
4040
|
-
return createDebuggerHandle(activeSession, tracker.emit, lifecycle);
|
|
4632
|
+
return await completeStartup(plan, deadline, resources);
|
|
4041
4633
|
} catch (error) {
|
|
4042
|
-
|
|
4043
|
-
deadline.dispose();
|
|
4044
|
-
const normalized = normalizeStartupError(error, deadline.expiresAt, startupTimeoutMs);
|
|
4045
|
-
if (lifecycle === void 0) {
|
|
4046
|
-
tracker.emit(
|
|
4047
|
-
"error",
|
|
4048
|
-
normalized instanceof Error ? normalized.message : String(normalized)
|
|
4049
|
-
);
|
|
4050
|
-
throw normalized;
|
|
4051
|
-
}
|
|
4052
|
-
return await cleanupFailedStartup(normalized, lifecycle, tracker.emit);
|
|
4634
|
+
return await handleStartupFailure(error, plan, resources, deadline);
|
|
4053
4635
|
}
|
|
4054
4636
|
}
|
|
4055
4637
|
async function startDebuggerWithinDeadline(options, deadline) {
|
|
@@ -4068,31 +4650,261 @@ async function startDebugger(options) {
|
|
|
4068
4650
|
|
|
4069
4651
|
// src/debug-session/doctor.ts
|
|
4070
4652
|
import {
|
|
4071
|
-
lstat,
|
|
4072
|
-
readFile as
|
|
4073
|
-
readdir as
|
|
4653
|
+
lstat as lstat3,
|
|
4654
|
+
readFile as readFile7,
|
|
4655
|
+
readdir as readdir5,
|
|
4074
4656
|
unlink as unlink5
|
|
4075
4657
|
} from "fs/promises";
|
|
4076
|
-
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";
|
|
4077
4664
|
import { join as join3 } from "path";
|
|
4078
|
-
|
|
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
|
|
4079
4894
|
var MANAGED_PORT_MIN = 2e4;
|
|
4080
4895
|
var MANAGED_PORT_MAX = 20999;
|
|
4081
4896
|
var PORT_SCAN_CONCURRENCY = 32;
|
|
4082
4897
|
var STALE_TEMP_AGE_MS2 = 24 * 60 * 60 * 1e3;
|
|
4083
4898
|
var STALE_LOCK_AGE_MS = 60 * 60 * 1e3;
|
|
4084
4899
|
var FOREIGN_OR_MALFORMED_LOCK_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
4085
|
-
|
|
4086
|
-
var LEGACY_LOCK_FILENAME = "cf-debugger-state.lock";
|
|
4087
|
-
var LEGACY_HOMES_DIRNAME = "cf-debugger-homes";
|
|
4088
|
-
function errorCode7(error) {
|
|
4900
|
+
function errorCode9(error) {
|
|
4089
4901
|
if (typeof error !== "object" || error === null) {
|
|
4090
4902
|
return void 0;
|
|
4091
4903
|
}
|
|
4092
4904
|
const value = Reflect.get(error, "code");
|
|
4093
4905
|
return typeof value === "string" ? value : void 0;
|
|
4094
4906
|
}
|
|
4095
|
-
function
|
|
4907
|
+
function errorMessage2(error) {
|
|
4096
4908
|
return error instanceof Error ? error.message : String(error);
|
|
4097
4909
|
}
|
|
4098
4910
|
function emptyState2() {
|
|
@@ -4101,14 +4913,14 @@ function emptyState2() {
|
|
|
4101
4913
|
async function readDoctorState() {
|
|
4102
4914
|
let raw;
|
|
4103
4915
|
try {
|
|
4104
|
-
raw = await
|
|
4916
|
+
raw = await readFile7(stateFilePath(), "utf8");
|
|
4105
4917
|
} catch (error) {
|
|
4106
|
-
if (
|
|
4918
|
+
if (errorCode9(error) === "ENOENT") {
|
|
4107
4919
|
return { state: emptyState2(), warnings: [], homeCleanupSafe: true };
|
|
4108
4920
|
}
|
|
4109
4921
|
return {
|
|
4110
4922
|
state: emptyState2(),
|
|
4111
|
-
warnings: [`Could not read debugger state: ${
|
|
4923
|
+
warnings: [`Could not read debugger state: ${errorMessage2(error)}`],
|
|
4112
4924
|
homeCleanupSafe: false
|
|
4113
4925
|
};
|
|
4114
4926
|
}
|
|
@@ -4136,54 +4948,58 @@ async function readDoctorState() {
|
|
|
4136
4948
|
homeCleanupSafe: decoded.dropped.length === 0
|
|
4137
4949
|
};
|
|
4138
4950
|
}
|
|
4139
|
-
async function inspectSessions(sessions) {
|
|
4140
|
-
const local = sessions.filter((session) => session.hostname === hostname2());
|
|
4141
|
-
return await Promise.all(local.map(async (session) => {
|
|
4142
|
-
try {
|
|
4143
|
-
return { session, health: await inspectSessionHealth(session) };
|
|
4144
|
-
} catch (error) {
|
|
4145
|
-
return {
|
|
4146
|
-
session,
|
|
4147
|
-
health: {
|
|
4148
|
-
status: "unverified",
|
|
4149
|
-
reason: `health inspection failed: ${errorMessage(error)}`
|
|
4150
|
-
}
|
|
4151
|
-
};
|
|
4152
|
-
}
|
|
4153
|
-
}));
|
|
4154
|
-
}
|
|
4155
4951
|
async function readDirectory(path) {
|
|
4156
4952
|
try {
|
|
4157
|
-
return await
|
|
4953
|
+
return await readdir5(path, { withFileTypes: true });
|
|
4158
4954
|
} catch (error) {
|
|
4159
|
-
if (
|
|
4955
|
+
if (errorCode9(error) === "ENOENT") {
|
|
4160
4956
|
return [];
|
|
4161
4957
|
}
|
|
4162
4958
|
throw error;
|
|
4163
4959
|
}
|
|
4164
4960
|
}
|
|
4165
|
-
async function findOrphanHomes(sessions,
|
|
4166
|
-
const
|
|
4167
|
-
const
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
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
|
+
};
|
|
4178
5000
|
}
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
sessionId: entry.name,
|
|
4182
|
-
path,
|
|
4183
|
-
cleanupEligible,
|
|
4184
|
-
cleanupStatus
|
|
4185
|
-
};
|
|
4186
|
-
}));
|
|
5001
|
+
));
|
|
5002
|
+
return { findings, warnings: discovery.warnings };
|
|
4187
5003
|
}
|
|
4188
5004
|
async function cleanupOrphanHome(sessionId, path) {
|
|
4189
5005
|
try {
|
|
@@ -4224,7 +5040,7 @@ async function mapConcurrent(values, concurrency, work) {
|
|
|
4224
5040
|
}
|
|
4225
5041
|
function unclaimedManagedPorts(sessions) {
|
|
4226
5042
|
const localClaims = new Set(
|
|
4227
|
-
sessions.filter((session) => session.hostname ===
|
|
5043
|
+
sessions.filter((session) => session.hostname === hostname4()).map((session) => session.localPort)
|
|
4228
5044
|
);
|
|
4229
5045
|
const ports = [];
|
|
4230
5046
|
for (let port = MANAGED_PORT_MIN; port <= MANAGED_PORT_MAX; port += 1) {
|
|
@@ -4301,15 +5117,15 @@ function parseLockOwner2(raw) {
|
|
|
4301
5117
|
}
|
|
4302
5118
|
function isPidAlive2(pid) {
|
|
4303
5119
|
try {
|
|
4304
|
-
|
|
5120
|
+
nodeProcess10.kill(pid, 0);
|
|
4305
5121
|
return true;
|
|
4306
5122
|
} catch (error) {
|
|
4307
|
-
return
|
|
5123
|
+
return errorCode9(error) !== "ESRCH";
|
|
4308
5124
|
}
|
|
4309
5125
|
}
|
|
4310
5126
|
async function readLockOwner2(path) {
|
|
4311
5127
|
try {
|
|
4312
|
-
return parseLockOwner2(await
|
|
5128
|
+
return parseLockOwner2(await readFile7(path, "utf8"));
|
|
4313
5129
|
} catch {
|
|
4314
5130
|
return void 0;
|
|
4315
5131
|
}
|
|
@@ -4319,7 +5135,7 @@ async function lockCleanupEligible(path, ageMs) {
|
|
|
4319
5135
|
return false;
|
|
4320
5136
|
}
|
|
4321
5137
|
const owner = await readLockOwner2(path);
|
|
4322
|
-
if (owner?.hostname ===
|
|
5138
|
+
if (owner?.hostname === hostname4()) {
|
|
4323
5139
|
if (!isPidAlive2(owner.pid)) {
|
|
4324
5140
|
return true;
|
|
4325
5141
|
}
|
|
@@ -4349,18 +5165,21 @@ async function inspectArtifact(entry, now, claimedSessionIds, stopIntentCleanupS
|
|
|
4349
5165
|
if (kind === void 0) {
|
|
4350
5166
|
return void 0;
|
|
4351
5167
|
}
|
|
4352
|
-
const path =
|
|
4353
|
-
const stats = await
|
|
5168
|
+
const path = join5(saptoolsDir(), entry.name);
|
|
5169
|
+
const stats = await lstat3(path);
|
|
4354
5170
|
const ageMs = Math.max(0, now - stats.mtimeMs);
|
|
4355
5171
|
const regularFile = stats.isFile();
|
|
4356
5172
|
let sessionId;
|
|
4357
5173
|
let cleanupEligible = regularFile && kind === "state-temp" && ageMs >= STALE_TEMP_AGE_MS2;
|
|
5174
|
+
let cleanupBlocked = false;
|
|
4358
5175
|
if (regularFile && kind === "stop-intent") {
|
|
4359
5176
|
sessionId = entry.name.slice(
|
|
4360
5177
|
CF_DEBUGGER_STOP_INTENT_PREFIX.length,
|
|
4361
5178
|
-".stop".length
|
|
4362
5179
|
);
|
|
4363
|
-
|
|
5180
|
+
const otherwiseEligible = ageMs >= STALE_TEMP_AGE_MS2 && !claimedSessionIds.has(sessionId);
|
|
5181
|
+
cleanupEligible = stopIntentCleanupSafe && otherwiseEligible;
|
|
5182
|
+
cleanupBlocked = !stopIntentCleanupSafe && otherwiseEligible;
|
|
4364
5183
|
}
|
|
4365
5184
|
if (regularFile && (kind === "state-lock" || kind === "state-recovery")) {
|
|
4366
5185
|
cleanupEligible = await lockCleanupEligible(path, ageMs);
|
|
@@ -4370,13 +5189,14 @@ async function inspectArtifact(entry, now, claimedSessionIds, stopIntentCleanupS
|
|
|
4370
5189
|
path,
|
|
4371
5190
|
ageMs,
|
|
4372
5191
|
cleanupEligible,
|
|
5192
|
+
cleanupBlocked,
|
|
4373
5193
|
fingerprint: artifactFingerprint(stats),
|
|
4374
5194
|
...sessionId === void 0 ? {} : { sessionId }
|
|
4375
5195
|
};
|
|
4376
5196
|
}
|
|
4377
5197
|
async function candidateStillMatches(candidate) {
|
|
4378
5198
|
try {
|
|
4379
|
-
const stats = await
|
|
5199
|
+
const stats = await lstat3(candidate.path);
|
|
4380
5200
|
return artifactFingerprint(stats) === candidate.fingerprint;
|
|
4381
5201
|
} catch {
|
|
4382
5202
|
return false;
|
|
@@ -4408,12 +5228,20 @@ async function cleanupArtifact(candidate) {
|
|
|
4408
5228
|
await unlink5(candidate.path);
|
|
4409
5229
|
return { status: "removed" };
|
|
4410
5230
|
} catch (error) {
|
|
4411
|
-
if (
|
|
5231
|
+
if (errorCode9(error) === "ENOENT") {
|
|
4412
5232
|
return { status: "skipped" };
|
|
4413
5233
|
}
|
|
4414
|
-
return { status: "failed", error:
|
|
5234
|
+
return { status: "failed", error: errorMessage2(error) };
|
|
4415
5235
|
}
|
|
4416
5236
|
}
|
|
5237
|
+
async function resolveArtifactCleanup(candidate, cleanup) {
|
|
5238
|
+
if (!cleanup) {
|
|
5239
|
+
return {
|
|
5240
|
+
status: candidate.cleanupEligible ? "not-requested" : "not-eligible"
|
|
5241
|
+
};
|
|
5242
|
+
}
|
|
5243
|
+
return candidate.cleanupBlocked ? { status: "skipped" } : await cleanupArtifact(candidate);
|
|
5244
|
+
}
|
|
4417
5245
|
async function findArtifacts(cleanup, sessions, stopIntentCleanupSafe) {
|
|
4418
5246
|
const entries = await readDirectory(saptoolsDir());
|
|
4419
5247
|
const claimedSessionIds = new Set(sessions.map((session) => session.sessionId));
|
|
@@ -4427,55 +5255,22 @@ async function findArtifacts(cleanup, sessions, stopIntentCleanupSafe) {
|
|
|
4427
5255
|
).sort((left, right) => left.path.localeCompare(right.path));
|
|
4428
5256
|
const findings = [];
|
|
4429
5257
|
for (const candidate of candidates) {
|
|
4430
|
-
const result =
|
|
4431
|
-
status: candidate.cleanupEligible ? "not-requested" : "not-eligible"
|
|
4432
|
-
};
|
|
5258
|
+
const result = await resolveArtifactCleanup(candidate, cleanup);
|
|
4433
5259
|
findings.push({
|
|
4434
5260
|
kind: candidate.kind,
|
|
4435
5261
|
path: candidate.path,
|
|
4436
5262
|
ageMs: candidate.ageMs,
|
|
4437
5263
|
cleanupEligible: candidate.cleanupEligible,
|
|
4438
5264
|
cleanupStatus: result.status,
|
|
4439
|
-
...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
|
+
} : {}
|
|
4440
5270
|
});
|
|
4441
5271
|
}
|
|
4442
5272
|
return findings;
|
|
4443
5273
|
}
|
|
4444
|
-
async function pathExists(path) {
|
|
4445
|
-
try {
|
|
4446
|
-
await lstat(path);
|
|
4447
|
-
return true;
|
|
4448
|
-
} catch (error) {
|
|
4449
|
-
if (errorCode7(error) === "ENOENT") {
|
|
4450
|
-
return false;
|
|
4451
|
-
}
|
|
4452
|
-
throw error;
|
|
4453
|
-
}
|
|
4454
|
-
}
|
|
4455
|
-
function shellQuote(value) {
|
|
4456
|
-
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
4457
|
-
}
|
|
4458
|
-
async function findLegacyArtifacts() {
|
|
4459
|
-
const statePath = join3(saptoolsDir(), LEGACY_STATE_FILENAME);
|
|
4460
|
-
const lockPath = join3(saptoolsDir(), LEGACY_LOCK_FILENAME);
|
|
4461
|
-
const homesPath = join3(saptoolsDir(), LEGACY_HOMES_DIRNAME);
|
|
4462
|
-
const [statePresent, homesPresent] = await Promise.all([
|
|
4463
|
-
pathExists(statePath),
|
|
4464
|
-
pathExists(homesPath)
|
|
4465
|
-
]);
|
|
4466
|
-
if (!statePresent && !homesPresent) {
|
|
4467
|
-
return { statePath, statePresent, homesPath, homesPresent };
|
|
4468
|
-
}
|
|
4469
|
-
const command = `rm -rf -- ${shellQuote(homesPath)} ${shellQuote(statePath)} ${shellQuote(lockPath)}`;
|
|
4470
|
-
return {
|
|
4471
|
-
statePath,
|
|
4472
|
-
statePresent,
|
|
4473
|
-
homesPath,
|
|
4474
|
-
homesPresent,
|
|
4475
|
-
warning: "Legacy v1 debugger homes may contain live CF refresh and access tokens. Confirm no v1 tunnel is running before removing them.",
|
|
4476
|
-
manualRemovalCommand: command
|
|
4477
|
-
};
|
|
4478
|
-
}
|
|
4479
5274
|
function reportWarnings(stateWarnings, orphanHomes, unclaimedPorts, legacy) {
|
|
4480
5275
|
const warnings = [...stateWarnings];
|
|
4481
5276
|
if (orphanHomes.length > 0) {
|
|
@@ -4492,14 +5287,14 @@ function reportWarnings(stateWarnings, orphanHomes, unclaimedPorts, legacy) {
|
|
|
4492
5287
|
async function runDoctor(options = {}) {
|
|
4493
5288
|
const cleanup = options.cleanup === true;
|
|
4494
5289
|
const stateResult = await readDoctorState();
|
|
4495
|
-
const
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
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),
|
|
4499
5293
|
findUnclaimedPorts(stateResult.state.sessions),
|
|
4500
5294
|
findArtifacts(cleanup, stateResult.state.sessions, stateResult.homeCleanupSafe),
|
|
4501
5295
|
findLegacyArtifacts()
|
|
4502
5296
|
]);
|
|
5297
|
+
const orphanHomes = homeInspection.findings;
|
|
4503
5298
|
const cleanedPaths = [
|
|
4504
5299
|
...orphanHomes.filter((finding) => finding.cleanupStatus === "removed").map((finding) => finding.path),
|
|
4505
5300
|
...artifacts.filter((finding) => finding.cleanupStatus === "removed").map((finding) => finding.path)
|
|
@@ -4512,6 +5307,7 @@ async function runDoctor(options = {}) {
|
|
|
4512
5307
|
legacy,
|
|
4513
5308
|
warnings: [
|
|
4514
5309
|
...reportWarnings(stateResult.warnings, orphanHomes, unclaimedPorts, legacy),
|
|
5310
|
+
...homeInspection.warnings,
|
|
4515
5311
|
...cleanup && !stateResult.homeCleanupSafe ? [
|
|
4516
5312
|
"Skipped orphan-home and stop-intent cleanup because debugger state was incomplete or invalid."
|
|
4517
5313
|
] : []
|
|
@@ -4522,7 +5318,7 @@ async function runDoctor(options = {}) {
|
|
|
4522
5318
|
|
|
4523
5319
|
// src/debug-session/sessions.ts
|
|
4524
5320
|
import { hostname as getHostname4 } from "os";
|
|
4525
|
-
import
|
|
5321
|
+
import nodeProcess11 from "process";
|
|
4526
5322
|
function findMatchingSession(sessions, options) {
|
|
4527
5323
|
if (options.sessionId !== void 0) {
|
|
4528
5324
|
return sessions.find((session) => session.sessionId === options.sessionId);
|
|
@@ -4540,50 +5336,55 @@ function findMatchingSession(sessions, options) {
|
|
|
4540
5336
|
}
|
|
4541
5337
|
return matches[0];
|
|
4542
5338
|
}
|
|
4543
|
-
function startupAgeLimit2(session) {
|
|
4544
|
-
return (session.startupTimeoutMs ?? MAX_STARTUP_TIMEOUT_MS) + STARTUP_STALE_SLACK_MS;
|
|
4545
|
-
}
|
|
4546
|
-
function startupExpired2(session) {
|
|
4547
|
-
const startedAt = Date.parse(session.startedAt);
|
|
4548
|
-
return Number.isNaN(startedAt) || Date.now() - startedAt > startupAgeLimit2(session);
|
|
4549
|
-
}
|
|
4550
|
-
async function inspectRecordedProcess2(pid, identity) {
|
|
4551
|
-
if (!isPidAlive(pid)) {
|
|
4552
|
-
return "dead";
|
|
4553
|
-
}
|
|
4554
|
-
return await inspectProcessIdentity(pid, identity);
|
|
4555
|
-
}
|
|
4556
5339
|
async function inspectController(target) {
|
|
4557
5340
|
const controllerPid = target.controllerPid ?? target.pid;
|
|
4558
|
-
return await
|
|
5341
|
+
return await inspectRecordedProcess(
|
|
5342
|
+
controllerPid,
|
|
5343
|
+
target.controllerProcessIdentity,
|
|
5344
|
+
isPidAlive
|
|
5345
|
+
);
|
|
4559
5346
|
}
|
|
4560
5347
|
async function ownsRecordedTunnel(target) {
|
|
4561
5348
|
const tunnelPid = target.tunnelPid;
|
|
4562
5349
|
if (tunnelPid === void 0) {
|
|
4563
5350
|
return false;
|
|
4564
5351
|
}
|
|
4565
|
-
const processVerdict = await
|
|
5352
|
+
const processVerdict = await inspectRecordedProcess(
|
|
4566
5353
|
tunnelPid,
|
|
4567
|
-
target.tunnelProcessIdentity
|
|
5354
|
+
target.tunnelProcessIdentity,
|
|
5355
|
+
isPidAlive
|
|
4568
5356
|
);
|
|
4569
5357
|
if (processVerdict !== "match") {
|
|
4570
5358
|
return false;
|
|
4571
5359
|
}
|
|
4572
5360
|
return (await inspectPortOwnership(target.localPort, tunnelPid)).status === "owned";
|
|
4573
5361
|
}
|
|
4574
|
-
async function terminateVerifiedTunnel(target) {
|
|
5362
|
+
async function terminateVerifiedTunnel(target, recordExpectedStop) {
|
|
4575
5363
|
const tunnelPid = target.tunnelPid;
|
|
4576
|
-
if (tunnelPid === void 0 || tunnelPid ===
|
|
4577
|
-
return tunnelPid ===
|
|
5364
|
+
if (tunnelPid === void 0 || tunnelPid === nodeProcess11.pid) {
|
|
5365
|
+
return tunnelPid === nodeProcess11.pid ? "still-alive" : "terminated";
|
|
4578
5366
|
}
|
|
5367
|
+
let stopIntentRecorded = false;
|
|
4579
5368
|
try {
|
|
4580
5369
|
const verifyBeforeSignal = async (signal) => {
|
|
4581
5370
|
if (signal === "SIGTERM" || target.tunnelProcessIdentity === void 0) {
|
|
4582
|
-
|
|
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;
|
|
4583
5383
|
}
|
|
4584
|
-
return await
|
|
5384
|
+
return await inspectRecordedProcess(
|
|
4585
5385
|
tunnelPid,
|
|
4586
|
-
target.tunnelProcessIdentity
|
|
5386
|
+
target.tunnelProcessIdentity,
|
|
5387
|
+
isPidAlive
|
|
4587
5388
|
) === "match";
|
|
4588
5389
|
};
|
|
4589
5390
|
return await terminatePidOrGroup(
|
|
@@ -4597,13 +5398,10 @@ async function terminateVerifiedTunnel(target) {
|
|
|
4597
5398
|
}
|
|
4598
5399
|
}
|
|
4599
5400
|
async function terminateVerifiedTunnelAndConfirm(target, recordExpectedStop = false) {
|
|
4600
|
-
if (recordExpectedStop) {
|
|
4601
|
-
await writeSessionStopIntent(target.sessionId);
|
|
4602
|
-
}
|
|
4603
5401
|
if (!await ownsRecordedTunnel(target)) {
|
|
4604
5402
|
throw ownershipError(target, "recorded tunnel no longer owns the local port");
|
|
4605
5403
|
}
|
|
4606
|
-
const termination = await terminateVerifiedTunnel(target);
|
|
5404
|
+
const termination = await terminateVerifiedTunnel(target, recordExpectedStop);
|
|
4607
5405
|
if (termination !== "terminated" || await ownsRecordedTunnel(target)) {
|
|
4608
5406
|
throw new CfDebuggerError(
|
|
4609
5407
|
"TUNNEL_TERMINATION_FAILED",
|
|
@@ -4613,18 +5411,26 @@ async function terminateVerifiedTunnelAndConfirm(target, recordExpectedStop = fa
|
|
|
4613
5411
|
}
|
|
4614
5412
|
async function removeOwnedSession(target, stale, forced = false, warning, preserveStopIntent = false) {
|
|
4615
5413
|
let resultWarning = warning;
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
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") {
|
|
4619
5419
|
const skipped = `State referenced unowned CF home ${target.cfHomeDir}; it was not deleted.`;
|
|
4620
|
-
resultWarning = resultWarning
|
|
5420
|
+
resultWarning = appendWarning(resultWarning, skipped);
|
|
4621
5421
|
}
|
|
4622
|
-
const removed = await removeSession(target.sessionId);
|
|
4623
5422
|
if (!preserveStopIntent) {
|
|
4624
|
-
|
|
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
|
+
}
|
|
4625
5431
|
}
|
|
4626
5432
|
return {
|
|
4627
|
-
...removed ?? target,
|
|
5433
|
+
...cleanup.removed ?? target,
|
|
4628
5434
|
stale,
|
|
4629
5435
|
pending: false,
|
|
4630
5436
|
forced,
|
|
@@ -4640,6 +5446,9 @@ function ownershipError(target, detail) {
|
|
|
4640
5446
|
function forcedWarning(target, detail) {
|
|
4641
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.`;
|
|
4642
5448
|
}
|
|
5449
|
+
function appendWarning(current, next) {
|
|
5450
|
+
return current === void 0 ? next : `${current} ${next}`;
|
|
5451
|
+
}
|
|
4643
5452
|
async function forceRemoveUnverified(target, detail, preserveStopIntent = false) {
|
|
4644
5453
|
return await removeOwnedSession(
|
|
4645
5454
|
target,
|
|
@@ -4654,20 +5463,24 @@ async function stopReadySession(target, force) {
|
|
|
4654
5463
|
await terminateVerifiedTunnelAndConfirm(target, true);
|
|
4655
5464
|
return await removeOwnedSession(target, false, false, void 0, true);
|
|
4656
5465
|
}
|
|
4657
|
-
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
|
+
);
|
|
4658
5471
|
const tunnelDead = target.tunnelPid === void 0 || !isPidOrGroupAlive(target.tunnelPid) || tunnelVerdict === "mismatch";
|
|
4659
5472
|
const ownership = target.tunnelPid === void 0 ? await inspectListeningProcesses(target.localPort) : await inspectPortOwnership(target.localPort, target.tunnelPid);
|
|
4660
5473
|
if (tunnelDead && ownership.status === "not-listening") {
|
|
4661
|
-
return await removeOwnedSession(target, true
|
|
5474
|
+
return await removeOwnedSession(target, true);
|
|
4662
5475
|
}
|
|
4663
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";
|
|
4664
5477
|
if (force) {
|
|
4665
|
-
return await forceRemoveUnverified(target, detail
|
|
5478
|
+
return await forceRemoveUnverified(target, detail);
|
|
4666
5479
|
}
|
|
4667
5480
|
throw ownershipError(target, detail);
|
|
4668
5481
|
}
|
|
4669
5482
|
async function stopStartingSession(target, force) {
|
|
4670
|
-
const expired =
|
|
5483
|
+
const expired = startupExpired(target);
|
|
4671
5484
|
const controllerVerdict = await inspectController(target);
|
|
4672
5485
|
if (!force && !expired && controllerVerdict === "match") {
|
|
4673
5486
|
return { ...target, stale: false, pending: true, forced: false };
|
|
@@ -4699,17 +5512,20 @@ async function stopDebugger(options) {
|
|
|
4699
5512
|
if (target === void 0) {
|
|
4700
5513
|
return void 0;
|
|
4701
5514
|
}
|
|
5515
|
+
if (target.status === "ready") {
|
|
5516
|
+
return await stopReadySession(target, options.force === true);
|
|
5517
|
+
}
|
|
4702
5518
|
const claim = await requestSessionStop(target.sessionId);
|
|
4703
5519
|
if (claim === void 0) {
|
|
4704
5520
|
return void 0;
|
|
4705
5521
|
}
|
|
4706
|
-
return
|
|
5522
|
+
return await stopStartingSession(claim.session, options.force === true);
|
|
4707
5523
|
}
|
|
4708
5524
|
function outcomeForResult(result) {
|
|
4709
5525
|
return {
|
|
4710
5526
|
sessionId: result.sessionId,
|
|
4711
5527
|
app: result.app,
|
|
4712
|
-
status: result.pending ? "pending" : result.stale ? "stale" : "stopped",
|
|
5528
|
+
status: result.pending ? "pending" : result.forced ? "forced" : result.stale ? "stale" : "stopped",
|
|
4713
5529
|
result
|
|
4714
5530
|
};
|
|
4715
5531
|
}
|
|
@@ -4737,6 +5553,7 @@ function summarizeOutcomes(outcomes) {
|
|
|
4737
5553
|
return {
|
|
4738
5554
|
outcomes,
|
|
4739
5555
|
failed: count("failed"),
|
|
5556
|
+
forced: count("forced"),
|
|
4740
5557
|
pending: count("pending"),
|
|
4741
5558
|
stale: count("stale"),
|
|
4742
5559
|
stopped: count("stopped")
|
|
@@ -4769,6 +5586,7 @@ async function getSession(key) {
|
|
|
4769
5586
|
|
|
4770
5587
|
export {
|
|
4771
5588
|
CfDebuggerError,
|
|
5589
|
+
validateApiEndpointOverride,
|
|
4772
5590
|
resolveApiEndpoint,
|
|
4773
5591
|
listKnownRegionKeys,
|
|
4774
5592
|
readCurrentCfTarget,
|
|
@@ -4780,10 +5598,12 @@ export {
|
|
|
4780
5598
|
resolveNodeTarget,
|
|
4781
5599
|
buildNodeInspectorCommand,
|
|
4782
5600
|
parseNodeInspectorMarkers,
|
|
5601
|
+
parseRestartEnvironment,
|
|
4783
5602
|
sessionKeyString,
|
|
4784
5603
|
CLEANUP_FAILURE_EXIT_CODE,
|
|
4785
5604
|
hasTunnelTerminationFailure,
|
|
4786
5605
|
cliErrorExitCode,
|
|
5606
|
+
stopAllExitCode,
|
|
4787
5607
|
getDebuggerHandleTunnelError,
|
|
4788
5608
|
resolveStartupTimeoutMs,
|
|
4789
5609
|
remainingStartupMs,
|
|
@@ -4797,4 +5617,4 @@ export {
|
|
|
4797
5617
|
listSessions,
|
|
4798
5618
|
getSession
|
|
4799
5619
|
};
|
|
4800
|
-
//# sourceMappingURL=chunk-
|
|
5620
|
+
//# sourceMappingURL=chunk-QBB4N2WN.js.map
|