@saptools/cf-debugger 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +261 -188
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +226 -151
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
4
|
+
import process6 from "process";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
|
-
// src/
|
|
7
|
+
// src/debug-session/start.ts
|
|
8
8
|
import { mkdir as mkdir3, rm } from "fs/promises";
|
|
9
|
-
import
|
|
10
|
-
import process3 from "process";
|
|
11
|
-
|
|
12
|
-
// src/cf.ts
|
|
13
|
-
import { execFile, spawn } from "child_process";
|
|
14
|
-
import { promisify } from "util";
|
|
9
|
+
import process4 from "process";
|
|
15
10
|
|
|
16
11
|
// src/types.ts
|
|
17
12
|
var CfDebuggerError = class extends Error {
|
|
@@ -27,19 +22,34 @@ var CfDebuggerError = class extends Error {
|
|
|
27
22
|
}
|
|
28
23
|
};
|
|
29
24
|
|
|
30
|
-
// src/
|
|
25
|
+
// src/cloud-foundry/execute.ts
|
|
26
|
+
import { execFile } from "child_process";
|
|
27
|
+
import { promisify } from "util";
|
|
31
28
|
var execFileAsync = promisify(execFile);
|
|
32
29
|
var MAX_BUFFER = 16 * 1024 * 1024;
|
|
33
30
|
var CF_CLI_TIMEOUT_MS = 3e4;
|
|
34
|
-
var
|
|
35
|
-
var CF_SSH_SIGNAL_TIMEOUT_MS = 15e3;
|
|
36
|
-
var CF_AUTH_MAX_ATTEMPTS = 3;
|
|
31
|
+
var REDACTED_ARG = "<redacted>";
|
|
37
32
|
function buildEnv(cfHome) {
|
|
38
33
|
return { ...process.env, CF_HOME: cfHome };
|
|
39
34
|
}
|
|
40
35
|
function resolveBin(context) {
|
|
41
36
|
return context.command ?? process.env["CF_DEBUGGER_CF_BIN"] ?? "cf";
|
|
42
37
|
}
|
|
38
|
+
function sensitiveArgs(args) {
|
|
39
|
+
if (args[0] !== "auth") {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
return args.slice(1).filter((arg) => arg.length > 0);
|
|
43
|
+
}
|
|
44
|
+
function redactText(text, values) {
|
|
45
|
+
return values.reduce((current, value) => current.split(value).join(REDACTED_ARG), text);
|
|
46
|
+
}
|
|
47
|
+
function formatArgsForError(args) {
|
|
48
|
+
if (args[0] !== "auth") {
|
|
49
|
+
return args.join(" ");
|
|
50
|
+
}
|
|
51
|
+
return args.map((arg, index) => index === 0 ? arg : REDACTED_ARG).join(" ");
|
|
52
|
+
}
|
|
43
53
|
async function runCf(args, context, timeoutMs = CF_CLI_TIMEOUT_MS) {
|
|
44
54
|
try {
|
|
45
55
|
const { stdout } = await execFileAsync(resolveBin(context), [...args], {
|
|
@@ -50,14 +60,20 @@ async function runCf(args, context, timeoutMs = CF_CLI_TIMEOUT_MS) {
|
|
|
50
60
|
return stdout;
|
|
51
61
|
} catch (err) {
|
|
52
62
|
const e = err;
|
|
53
|
-
const
|
|
63
|
+
const redactionValues = sensitiveArgs(args);
|
|
64
|
+
const stderr = redactText(e.stderr?.trim() ?? "", redactionValues);
|
|
65
|
+
const fallbackMessage = redactText(e.message, redactionValues);
|
|
54
66
|
throw new CfDebuggerError(
|
|
55
67
|
"CF_CLI_FAILED",
|
|
56
|
-
`cf ${args
|
|
68
|
+
`cf ${formatArgsForError(args)} failed: ${stderr.length > 0 ? stderr : fallbackMessage}`,
|
|
57
69
|
stderr
|
|
58
70
|
);
|
|
59
71
|
}
|
|
60
72
|
}
|
|
73
|
+
|
|
74
|
+
// src/cloud-foundry/commands.ts
|
|
75
|
+
var CF_RESTART_TIMEOUT_MS = 12e4;
|
|
76
|
+
var CF_AUTH_MAX_ATTEMPTS = 3;
|
|
61
77
|
async function cfApi(apiEndpoint, context) {
|
|
62
78
|
await runCf(["api", apiEndpoint], context);
|
|
63
79
|
}
|
|
@@ -123,6 +139,10 @@ async function cfEnableSsh(appName, context) {
|
|
|
123
139
|
async function cfRestartApp(appName, context) {
|
|
124
140
|
await runCf(["restart", appName], context, CF_RESTART_TIMEOUT_MS);
|
|
125
141
|
}
|
|
142
|
+
|
|
143
|
+
// src/cloud-foundry/ssh.ts
|
|
144
|
+
import { spawn } from "child_process";
|
|
145
|
+
var CF_SSH_SIGNAL_TIMEOUT_MS = 15e3;
|
|
126
146
|
async function cfSshOneShot(appName, command, context) {
|
|
127
147
|
return await new Promise((resolve) => {
|
|
128
148
|
const child = spawn(resolveBin(context), ["ssh", appName, "-c", command], {
|
|
@@ -197,7 +217,7 @@ function sessionCfHomeDir(sessionId) {
|
|
|
197
217
|
return join(saptoolsDir(), CF_DEBUGGER_HOMES_DIRNAME, sessionId);
|
|
198
218
|
}
|
|
199
219
|
|
|
200
|
-
// src/
|
|
220
|
+
// src/network/ports.ts
|
|
201
221
|
import { execFile as execFile2 } from "child_process";
|
|
202
222
|
import { createConnection, createServer } from "net";
|
|
203
223
|
import { promisify as promisify2 } from "util";
|
|
@@ -366,7 +386,7 @@ function resolveApiEndpoint(regionKey, override) {
|
|
|
366
386
|
return endpoint;
|
|
367
387
|
}
|
|
368
388
|
|
|
369
|
-
// src/state.ts
|
|
389
|
+
// src/session-state/store.ts
|
|
370
390
|
import { randomUUID } from "crypto";
|
|
371
391
|
import { mkdir as mkdir2, readFile, rename, writeFile } from "fs/promises";
|
|
372
392
|
import { hostname as getHostname } from "os";
|
|
@@ -421,7 +441,7 @@ async function withFileLock(lockPath, work, options) {
|
|
|
421
441
|
}
|
|
422
442
|
}
|
|
423
443
|
|
|
424
|
-
// src/state.ts
|
|
444
|
+
// src/session-state/store.ts
|
|
425
445
|
async function readJsonFile(path) {
|
|
426
446
|
let raw;
|
|
427
447
|
try {
|
|
@@ -653,13 +673,29 @@ async function removeSession(sessionId) {
|
|
|
653
673
|
});
|
|
654
674
|
}
|
|
655
675
|
|
|
656
|
-
// src/
|
|
676
|
+
// src/debug-session/constants.ts
|
|
657
677
|
var DEFAULT_TUNNEL_READY_TIMEOUT_MS = 3e4;
|
|
658
678
|
var POST_USR1_DELAY_MS = 300;
|
|
659
679
|
var PORT_CLEANUP_DELAY_MS = 600;
|
|
660
680
|
var CHILD_SIGTERM_GRACE_MS = 2e3;
|
|
661
681
|
var PORT_RECLAIM_DELAY_MS = 250;
|
|
662
682
|
var PID_TERMINATION_POLL_MS = 100;
|
|
683
|
+
|
|
684
|
+
// src/debug-session/orphans.ts
|
|
685
|
+
import { hostname as getHostname2 } from "os";
|
|
686
|
+
async function pruneAndCleanupOrphans() {
|
|
687
|
+
const result = await readAndPruneActiveSessions();
|
|
688
|
+
const host = getHostname2();
|
|
689
|
+
for (const removed of result.removed) {
|
|
690
|
+
if (removed.hostname === host) {
|
|
691
|
+
void killProcessOnPort(removed.localPort);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return result.sessions;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/debug-session/processes.ts
|
|
698
|
+
import process3 from "process";
|
|
663
699
|
function signalPidOrGroup(pid, signal) {
|
|
664
700
|
const isWindows = process3.platform === "win32";
|
|
665
701
|
if (!isWindows) {
|
|
@@ -699,24 +735,16 @@ async function killProcessGroupOrProc(child, timeoutMs = CHILD_SIGTERM_GRACE_MS)
|
|
|
699
735
|
}
|
|
700
736
|
await terminatePidOrGroup(child.pid, timeoutMs);
|
|
701
737
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
const host = getHostname2();
|
|
705
|
-
for (const removed of result.removed) {
|
|
706
|
-
if (removed.hostname === host) {
|
|
707
|
-
void killProcessOnPort(removed.localPort);
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
return result.sessions;
|
|
711
|
-
}
|
|
738
|
+
|
|
739
|
+
// src/debug-session/start.ts
|
|
712
740
|
function checkAbort(signal) {
|
|
713
741
|
if (signal?.aborted) {
|
|
714
742
|
throw new CfDebuggerError("ABORTED", "Operation aborted by caller");
|
|
715
743
|
}
|
|
716
744
|
}
|
|
717
745
|
function requireCredentials(options) {
|
|
718
|
-
const email = options.email ??
|
|
719
|
-
const password = options.password ??
|
|
746
|
+
const email = options.email ?? process4.env["SAP_EMAIL"];
|
|
747
|
+
const password = options.password ?? process4.env["SAP_PASSWORD"];
|
|
720
748
|
if (email === void 0 || email === "") {
|
|
721
749
|
throw new CfDebuggerError(
|
|
722
750
|
"MISSING_CREDENTIALS",
|
|
@@ -731,15 +759,7 @@ function requireCredentials(options) {
|
|
|
731
759
|
}
|
|
732
760
|
return { email, password };
|
|
733
761
|
}
|
|
734
|
-
async function
|
|
735
|
-
const { email, password } = requireCredentials(options);
|
|
736
|
-
const apiEndpoint = resolveApiEndpoint(options.region, options.apiEndpoint);
|
|
737
|
-
const tunnelReadyTimeoutMs = options.tunnelReadyTimeoutMs ?? DEFAULT_TUNNEL_READY_TIMEOUT_MS;
|
|
738
|
-
const emit = (status, message) => {
|
|
739
|
-
options.onStatus?.(status, message);
|
|
740
|
-
};
|
|
741
|
-
checkAbort(options.signal);
|
|
742
|
-
await pruneAndCleanupOrphans();
|
|
762
|
+
async function registerSession(options, apiEndpoint) {
|
|
743
763
|
const registration = await registerNewSession({
|
|
744
764
|
region: options.region,
|
|
745
765
|
org: options.org,
|
|
@@ -756,20 +776,151 @@ async function startDebugger(options) {
|
|
|
756
776
|
`A debugger session is already running for ${sessionKeyString(options)} on port ${registration.existing.localPort.toString()} (pid ${registration.existing.pid.toString()}, sessionId ${registration.existing.sessionId}). Stop it first with \`cf-debugger stop\`.`
|
|
757
777
|
);
|
|
758
778
|
}
|
|
759
|
-
|
|
779
|
+
return registration.session;
|
|
780
|
+
}
|
|
781
|
+
async function loginAndTarget(options, apiEndpoint, email, password, context, sessionId, emit) {
|
|
782
|
+
emit("logging-in");
|
|
783
|
+
await updateSessionStatus(sessionId, "logging-in");
|
|
784
|
+
await cfLogin(apiEndpoint, email, password, context);
|
|
785
|
+
checkAbort(options.signal);
|
|
786
|
+
emit("targeting");
|
|
787
|
+
await updateSessionStatus(sessionId, "targeting");
|
|
788
|
+
await cfTarget(options.org, options.space, context);
|
|
789
|
+
checkAbort(options.signal);
|
|
790
|
+
}
|
|
791
|
+
async function signalRemoteNode(options, context, sessionId, emit) {
|
|
792
|
+
emit("signaling");
|
|
793
|
+
await updateSessionStatus(sessionId, "signaling");
|
|
794
|
+
const signalResult = await cfSshOneShot(options.app, `kill -s USR1 $(pidof node)`, context);
|
|
795
|
+
if (!isSshDisabledError(signalResult.stderr)) {
|
|
796
|
+
if (signalResult.exitCode === 0) {
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
const detail = signalResult.stderr.trim().length > 0 ? signalResult.stderr.trim() : `exit code ${String(signalResult.exitCode)}`;
|
|
800
|
+
throw new CfDebuggerError(
|
|
801
|
+
"USR1_SIGNAL_FAILED",
|
|
802
|
+
`Failed to send SIGUSR1 to the Node.js process on ${options.app}: ${detail}`,
|
|
803
|
+
signalResult.stderr
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
const alreadyEnabled = await cfSshEnabled(options.app, context);
|
|
807
|
+
if (!alreadyEnabled) {
|
|
808
|
+
emit("ssh-enabling", "Enabling SSH on the app");
|
|
809
|
+
await updateSessionStatus(sessionId, "ssh-enabling");
|
|
810
|
+
await cfEnableSsh(options.app, context);
|
|
811
|
+
}
|
|
812
|
+
emit("ssh-restarting", "Restarting app so SSH becomes active");
|
|
813
|
+
await updateSessionStatus(sessionId, "ssh-restarting");
|
|
814
|
+
await cfRestartApp(options.app, context);
|
|
815
|
+
checkAbort(options.signal);
|
|
816
|
+
await retryRemoteSignal(options, context, sessionId, emit);
|
|
817
|
+
}
|
|
818
|
+
async function retryRemoteSignal(options, context, sessionId, emit) {
|
|
819
|
+
emit("signaling");
|
|
820
|
+
await updateSessionStatus(sessionId, "signaling");
|
|
821
|
+
const retrySignalResult = await cfSshOneShot(
|
|
822
|
+
options.app,
|
|
823
|
+
`kill -s USR1 $(pidof node)`,
|
|
824
|
+
context
|
|
825
|
+
);
|
|
826
|
+
if (retrySignalResult.exitCode === 0) {
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const detail = retrySignalResult.stderr.trim().length > 0 ? retrySignalResult.stderr.trim() : `exit code ${String(retrySignalResult.exitCode)}`;
|
|
830
|
+
throw new CfDebuggerError(
|
|
831
|
+
"USR1_SIGNAL_FAILED",
|
|
832
|
+
`Failed to send SIGUSR1 to the Node.js process on ${options.app} after enabling SSH: ${detail}`,
|
|
833
|
+
retrySignalResult.stderr
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
async function waitAfterSignal(signal) {
|
|
837
|
+
await new Promise((resolve) => {
|
|
838
|
+
setTimeout(resolve, POST_USR1_DELAY_MS);
|
|
839
|
+
});
|
|
840
|
+
checkAbort(signal);
|
|
841
|
+
}
|
|
842
|
+
async function ensurePortAvailable(localPort) {
|
|
843
|
+
if (await isPortFree(localPort)) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
await killProcessOnPort(localPort);
|
|
847
|
+
await new Promise((resolve) => {
|
|
848
|
+
setTimeout(resolve, PORT_RECLAIM_DELAY_MS);
|
|
849
|
+
});
|
|
850
|
+
if (!await isPortFree(localPort)) {
|
|
851
|
+
throw new CfDebuggerError(
|
|
852
|
+
"PORT_UNAVAILABLE",
|
|
853
|
+
`Local port ${localPort.toString()} is in use and could not be reclaimed for the tunnel.`
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
async function openReadyTunnel(options, session, context, tunnelReadyTimeoutMs, onChild) {
|
|
858
|
+
await ensurePortAvailable(session.localPort);
|
|
859
|
+
const child = spawnSshTunnel(options.app, session.localPort, session.remotePort, context);
|
|
860
|
+
onChild(child);
|
|
861
|
+
if (child.pid !== void 0) {
|
|
862
|
+
await updateSessionPid(session.sessionId, child.pid);
|
|
863
|
+
}
|
|
864
|
+
const ready = await probeTunnelReady(session.localPort, tunnelReadyTimeoutMs);
|
|
865
|
+
checkAbort(options.signal);
|
|
866
|
+
if (!ready) {
|
|
867
|
+
throw new CfDebuggerError(
|
|
868
|
+
"TUNNEL_NOT_READY",
|
|
869
|
+
`SSH tunnel on port ${session.localPort.toString()} did not become ready within ${Math.round(tunnelReadyTimeoutMs / 1e3).toString()}s.`
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
const listeningPid = await findListeningProcessId(session.localPort);
|
|
873
|
+
const activePid = listeningPid ?? child.pid ?? session.pid;
|
|
874
|
+
if (activePid !== session.pid) {
|
|
875
|
+
await updateSessionPid(session.sessionId, activePid);
|
|
876
|
+
}
|
|
877
|
+
return { child, activePid };
|
|
878
|
+
}
|
|
879
|
+
function attachTunnelEvents(child, markClosed, resolveExit, emit) {
|
|
880
|
+
child.on("close", (code) => {
|
|
881
|
+
markClosed();
|
|
882
|
+
resolveExit(code);
|
|
883
|
+
});
|
|
884
|
+
child.on("error", (err) => {
|
|
885
|
+
emit("error", err.message);
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
function createHandle(session, emit, finalize, exitPromise) {
|
|
889
|
+
let disposePromise;
|
|
890
|
+
return {
|
|
891
|
+
session,
|
|
892
|
+
dispose: async () => {
|
|
893
|
+
disposePromise ??= (async () => {
|
|
894
|
+
emit("stopping");
|
|
895
|
+
await updateSessionStatus(session.sessionId, "stopping");
|
|
896
|
+
await finalize();
|
|
897
|
+
})();
|
|
898
|
+
await disposePromise;
|
|
899
|
+
},
|
|
900
|
+
waitForExit: async () => {
|
|
901
|
+
return await exitPromise;
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
async function startDebugger(options) {
|
|
906
|
+
const { email, password } = requireCredentials(options);
|
|
907
|
+
const apiEndpoint = resolveApiEndpoint(options.region, options.apiEndpoint);
|
|
908
|
+
const tunnelReadyTimeoutMs = options.tunnelReadyTimeoutMs ?? DEFAULT_TUNNEL_READY_TIMEOUT_MS;
|
|
909
|
+
const emit = (status, message) => {
|
|
910
|
+
options.onStatus?.(status, message);
|
|
911
|
+
};
|
|
912
|
+
checkAbort(options.signal);
|
|
913
|
+
await pruneAndCleanupOrphans();
|
|
914
|
+
const session = await registerSession(options, apiEndpoint);
|
|
760
915
|
const context = { cfHome: session.cfHomeDir };
|
|
761
916
|
let child;
|
|
762
917
|
let tunnelClosed = false;
|
|
763
|
-
let exitResolve
|
|
918
|
+
let exitResolve = (_code) => {
|
|
919
|
+
throw new Error("Exit resolver was used before initialization");
|
|
920
|
+
};
|
|
764
921
|
const exitPromise = new Promise((resolve) => {
|
|
765
922
|
exitResolve = resolve;
|
|
766
923
|
});
|
|
767
|
-
const cleanupFilesystem = async () => {
|
|
768
|
-
try {
|
|
769
|
-
await rm(session.cfHomeDir, { recursive: true, force: true });
|
|
770
|
-
} catch {
|
|
771
|
-
}
|
|
772
|
-
};
|
|
773
924
|
const finalize = async () => {
|
|
774
925
|
if (!tunnelClosed) {
|
|
775
926
|
tunnelClosed = true;
|
|
@@ -781,125 +932,37 @@ async function startDebugger(options) {
|
|
|
781
932
|
}, PORT_CLEANUP_DELAY_MS);
|
|
782
933
|
}
|
|
783
934
|
await removeSession(session.sessionId);
|
|
784
|
-
await cleanupFilesystem();
|
|
935
|
+
await cleanupFilesystem(session.cfHomeDir);
|
|
785
936
|
emit("stopped");
|
|
786
937
|
};
|
|
787
938
|
try {
|
|
788
939
|
await mkdir3(session.cfHomeDir, { recursive: true });
|
|
789
|
-
emit
|
|
790
|
-
await updateSessionStatus(session.sessionId, "logging-in");
|
|
791
|
-
await cfLogin(apiEndpoint, email, password, context);
|
|
792
|
-
checkAbort(options.signal);
|
|
793
|
-
emit("targeting");
|
|
794
|
-
await updateSessionStatus(session.sessionId, "targeting");
|
|
795
|
-
await cfTarget(options.org, options.space, context);
|
|
796
|
-
checkAbort(options.signal);
|
|
940
|
+
await loginAndTarget(options, apiEndpoint, email, password, context, session.sessionId, emit);
|
|
797
941
|
await killProcessOnPort(session.localPort);
|
|
798
942
|
await new Promise((resolve) => {
|
|
799
943
|
setTimeout(resolve, 200);
|
|
800
944
|
});
|
|
801
|
-
emit
|
|
802
|
-
await
|
|
803
|
-
const signalResult = await cfSshOneShot(
|
|
804
|
-
options.app,
|
|
805
|
-
`kill -s USR1 $(pidof node)`,
|
|
806
|
-
context
|
|
807
|
-
);
|
|
808
|
-
if (isSshDisabledError(signalResult.stderr)) {
|
|
809
|
-
const alreadyEnabled = await cfSshEnabled(options.app, context);
|
|
810
|
-
if (!alreadyEnabled) {
|
|
811
|
-
emit("ssh-enabling", "Enabling SSH on the app");
|
|
812
|
-
await updateSessionStatus(session.sessionId, "ssh-enabling");
|
|
813
|
-
await cfEnableSsh(options.app, context);
|
|
814
|
-
}
|
|
815
|
-
emit("ssh-restarting", "Restarting app so SSH becomes active");
|
|
816
|
-
await updateSessionStatus(session.sessionId, "ssh-restarting");
|
|
817
|
-
await cfRestartApp(options.app, context);
|
|
818
|
-
checkAbort(options.signal);
|
|
819
|
-
emit("signaling");
|
|
820
|
-
await updateSessionStatus(session.sessionId, "signaling");
|
|
821
|
-
const retrySignalResult = await cfSshOneShot(
|
|
822
|
-
options.app,
|
|
823
|
-
`kill -s USR1 $(pidof node)`,
|
|
824
|
-
context
|
|
825
|
-
);
|
|
826
|
-
if (retrySignalResult.exitCode !== 0) {
|
|
827
|
-
const detail = retrySignalResult.stderr.trim().length > 0 ? retrySignalResult.stderr.trim() : `exit code ${String(retrySignalResult.exitCode)}`;
|
|
828
|
-
throw new CfDebuggerError(
|
|
829
|
-
"USR1_SIGNAL_FAILED",
|
|
830
|
-
`Failed to send SIGUSR1 to the Node.js process on ${options.app} after enabling SSH: ${detail}`,
|
|
831
|
-
retrySignalResult.stderr
|
|
832
|
-
);
|
|
833
|
-
}
|
|
834
|
-
} else if (signalResult.exitCode !== 0) {
|
|
835
|
-
const detail = signalResult.stderr.trim().length > 0 ? signalResult.stderr.trim() : `exit code ${String(signalResult.exitCode)}`;
|
|
836
|
-
throw new CfDebuggerError(
|
|
837
|
-
"USR1_SIGNAL_FAILED",
|
|
838
|
-
`Failed to send SIGUSR1 to the Node.js process on ${options.app}: ${detail}`,
|
|
839
|
-
signalResult.stderr
|
|
840
|
-
);
|
|
841
|
-
}
|
|
842
|
-
await new Promise((resolve) => {
|
|
843
|
-
setTimeout(resolve, POST_USR1_DELAY_MS);
|
|
844
|
-
});
|
|
845
|
-
checkAbort(options.signal);
|
|
945
|
+
await signalRemoteNode(options, context, session.sessionId, emit);
|
|
946
|
+
await waitAfterSignal(options.signal);
|
|
846
947
|
emit("tunneling");
|
|
847
948
|
await updateSessionStatus(session.sessionId, "tunneling");
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
);
|
|
949
|
+
const tunnel = await openReadyTunnel(
|
|
950
|
+
options,
|
|
951
|
+
session,
|
|
952
|
+
context,
|
|
953
|
+
tunnelReadyTimeoutMs,
|
|
954
|
+
(tunnelChild) => {
|
|
955
|
+
child = tunnelChild;
|
|
956
|
+
attachTunnelEvents(tunnelChild, () => {
|
|
957
|
+
tunnelClosed = true;
|
|
958
|
+
}, exitResolve, emit);
|
|
858
959
|
}
|
|
859
|
-
|
|
860
|
-
child =
|
|
861
|
-
if (child.pid !== void 0) {
|
|
862
|
-
await updateSessionPid(session.sessionId, child.pid);
|
|
863
|
-
}
|
|
864
|
-
child.on("close", (code) => {
|
|
865
|
-
tunnelClosed = true;
|
|
866
|
-
exitResolve?.(code);
|
|
867
|
-
});
|
|
868
|
-
child.on("error", (err) => {
|
|
869
|
-
emit("error", err.message);
|
|
870
|
-
});
|
|
871
|
-
const ready = await probeTunnelReady(session.localPort, tunnelReadyTimeoutMs);
|
|
872
|
-
checkAbort(options.signal);
|
|
873
|
-
if (!ready) {
|
|
874
|
-
throw new CfDebuggerError(
|
|
875
|
-
"TUNNEL_NOT_READY",
|
|
876
|
-
`SSH tunnel on port ${session.localPort.toString()} did not become ready within ${Math.round(tunnelReadyTimeoutMs / 1e3).toString()}s.`
|
|
877
|
-
);
|
|
878
|
-
}
|
|
879
|
-
const listeningPid = await findListeningProcessId(session.localPort);
|
|
880
|
-
const activePid = listeningPid ?? child.pid ?? session.pid;
|
|
881
|
-
if (activePid !== session.pid) {
|
|
882
|
-
await updateSessionPid(session.sessionId, activePid);
|
|
883
|
-
}
|
|
960
|
+
);
|
|
961
|
+
child = tunnel.child;
|
|
884
962
|
emit("ready");
|
|
885
963
|
const readySession = await updateSessionStatus(session.sessionId, "ready");
|
|
886
|
-
const activeSession = readySession ?? { ...session, pid: activePid, status: "ready" };
|
|
887
|
-
|
|
888
|
-
const handle = {
|
|
889
|
-
session: activeSession,
|
|
890
|
-
dispose: async () => {
|
|
891
|
-
disposePromise ??= (async () => {
|
|
892
|
-
emit("stopping");
|
|
893
|
-
await updateSessionStatus(session.sessionId, "stopping");
|
|
894
|
-
await finalize();
|
|
895
|
-
})();
|
|
896
|
-
await disposePromise;
|
|
897
|
-
},
|
|
898
|
-
waitForExit: async () => {
|
|
899
|
-
return await exitPromise;
|
|
900
|
-
}
|
|
901
|
-
};
|
|
902
|
-
return handle;
|
|
964
|
+
const activeSession = readySession ?? { ...session, pid: tunnel.activePid, status: "ready" };
|
|
965
|
+
return createHandle(activeSession, emit, finalize, exitPromise);
|
|
903
966
|
} catch (err) {
|
|
904
967
|
const message = err instanceof Error ? err.message : String(err);
|
|
905
968
|
emit("error", message);
|
|
@@ -907,6 +970,16 @@ async function startDebugger(options) {
|
|
|
907
970
|
throw err;
|
|
908
971
|
}
|
|
909
972
|
}
|
|
973
|
+
async function cleanupFilesystem(cfHomeDir) {
|
|
974
|
+
try {
|
|
975
|
+
await rm(cfHomeDir, { recursive: true, force: true });
|
|
976
|
+
} catch {
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// src/debug-session/sessions.ts
|
|
981
|
+
import { rm as rm2 } from "fs/promises";
|
|
982
|
+
import process5 from "process";
|
|
910
983
|
async function stopDebugger(options) {
|
|
911
984
|
const sessions = await pruneAndCleanupOrphans();
|
|
912
985
|
let target;
|
|
@@ -919,7 +992,7 @@ async function stopDebugger(options) {
|
|
|
919
992
|
if (target === void 0) {
|
|
920
993
|
return void 0;
|
|
921
994
|
}
|
|
922
|
-
if (target.pid !==
|
|
995
|
+
if (target.pid !== process5.pid) {
|
|
923
996
|
try {
|
|
924
997
|
await terminatePidOrGroup(target.pid);
|
|
925
998
|
} catch {
|
|
@@ -930,7 +1003,7 @@ async function stopDebugger(options) {
|
|
|
930
1003
|
}, PORT_CLEANUP_DELAY_MS);
|
|
931
1004
|
const removed = await removeSession(target.sessionId);
|
|
932
1005
|
try {
|
|
933
|
-
await
|
|
1006
|
+
await rm2(target.cfHomeDir, { recursive: true, force: true });
|
|
934
1007
|
} catch {
|
|
935
1008
|
}
|
|
936
1009
|
return removed ?? target;
|
|
@@ -957,9 +1030,9 @@ async function getSession(key) {
|
|
|
957
1030
|
// src/cli.ts
|
|
958
1031
|
function readRequiredOption(value, flag) {
|
|
959
1032
|
if (value === void 0 || value === "") {
|
|
960
|
-
|
|
1033
|
+
process6.stderr.write(`Missing required option ${flag}
|
|
961
1034
|
`);
|
|
962
|
-
|
|
1035
|
+
process6.exit(1);
|
|
963
1036
|
}
|
|
964
1037
|
return value;
|
|
965
1038
|
}
|
|
@@ -969,9 +1042,9 @@ function parseOptionalPort(raw) {
|
|
|
969
1042
|
}
|
|
970
1043
|
const port = Number.parseInt(raw, 10);
|
|
971
1044
|
if (Number.isNaN(port) || port <= 0 || port > 65535) {
|
|
972
|
-
|
|
1045
|
+
process6.stderr.write(`Invalid port: ${raw}
|
|
973
1046
|
`);
|
|
974
|
-
|
|
1047
|
+
process6.exit(1);
|
|
975
1048
|
}
|
|
976
1049
|
return port;
|
|
977
1050
|
}
|
|
@@ -981,16 +1054,16 @@ function parseOptionalTimeout(raw) {
|
|
|
981
1054
|
}
|
|
982
1055
|
const seconds = Number.parseInt(raw, 10);
|
|
983
1056
|
if (Number.isNaN(seconds) || seconds <= 0) {
|
|
984
|
-
|
|
1057
|
+
process6.stderr.write(`Invalid timeout: ${raw}
|
|
985
1058
|
`);
|
|
986
|
-
|
|
1059
|
+
process6.exit(1);
|
|
987
1060
|
}
|
|
988
1061
|
return seconds * 1e3;
|
|
989
1062
|
}
|
|
990
1063
|
function logStatus(verbose, status, message) {
|
|
991
1064
|
if (verbose) {
|
|
992
1065
|
const suffix = message === void 0 ? "" : `: ${message}`;
|
|
993
|
-
|
|
1066
|
+
process6.stdout.write(`[cf-debugger] ${status}${suffix}
|
|
994
1067
|
`);
|
|
995
1068
|
}
|
|
996
1069
|
}
|
|
@@ -1005,17 +1078,17 @@ async function handleStart(opts) {
|
|
|
1005
1078
|
const abortController = new AbortController();
|
|
1006
1079
|
const onStartupSignal = (exitCode) => () => {
|
|
1007
1080
|
abortController.abort();
|
|
1008
|
-
|
|
1081
|
+
process6.stderr.write(`
|
|
1009
1082
|
Aborting startup for ${app}...
|
|
1010
1083
|
`);
|
|
1011
1084
|
setTimeout(() => {
|
|
1012
|
-
|
|
1085
|
+
process6.exit(exitCode);
|
|
1013
1086
|
}, 5e3).unref();
|
|
1014
1087
|
};
|
|
1015
1088
|
const startupSigint = onStartupSignal(130);
|
|
1016
1089
|
const startupSigterm = onStartupSignal(143);
|
|
1017
|
-
|
|
1018
|
-
|
|
1090
|
+
process6.on("SIGINT", startupSigint);
|
|
1091
|
+
process6.on("SIGTERM", startupSigterm);
|
|
1019
1092
|
let handle;
|
|
1020
1093
|
try {
|
|
1021
1094
|
handle = await startDebugger({
|
|
@@ -1032,10 +1105,10 @@ Aborting startup for ${app}...
|
|
|
1032
1105
|
}
|
|
1033
1106
|
});
|
|
1034
1107
|
} finally {
|
|
1035
|
-
|
|
1036
|
-
|
|
1108
|
+
process6.off("SIGINT", startupSigint);
|
|
1109
|
+
process6.off("SIGTERM", startupSigterm);
|
|
1037
1110
|
}
|
|
1038
|
-
|
|
1111
|
+
process6.stdout.write(
|
|
1039
1112
|
`Debugger ready for ${app} (${region}/${org}/${space}).
|
|
1040
1113
|
Local port: ${handle.session.localPort.toString()}
|
|
1041
1114
|
Remote port: ${handle.session.remotePort.toString()}
|
|
@@ -1047,32 +1120,32 @@ Press Ctrl+C to stop.
|
|
|
1047
1120
|
let disposePromise;
|
|
1048
1121
|
const dispose = async () => {
|
|
1049
1122
|
disposePromise ??= (async () => {
|
|
1050
|
-
|
|
1123
|
+
process6.stdout.write(`
|
|
1051
1124
|
Stopping debugger for ${app}...
|
|
1052
1125
|
`);
|
|
1053
1126
|
try {
|
|
1054
1127
|
await handle.dispose();
|
|
1055
1128
|
} catch (err) {
|
|
1056
1129
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1057
|
-
|
|
1130
|
+
process6.stderr.write(`Error during stop: ${msg}
|
|
1058
1131
|
`);
|
|
1059
1132
|
}
|
|
1060
1133
|
})();
|
|
1061
1134
|
await disposePromise;
|
|
1062
1135
|
};
|
|
1063
|
-
|
|
1136
|
+
process6.on("SIGINT", () => {
|
|
1064
1137
|
void dispose().then(() => {
|
|
1065
|
-
|
|
1138
|
+
process6.exit(130);
|
|
1066
1139
|
});
|
|
1067
1140
|
});
|
|
1068
|
-
|
|
1141
|
+
process6.on("SIGTERM", () => {
|
|
1069
1142
|
void dispose().then(() => {
|
|
1070
|
-
|
|
1143
|
+
process6.exit(143);
|
|
1071
1144
|
});
|
|
1072
1145
|
});
|
|
1073
1146
|
const code = await handle.waitForExit();
|
|
1074
1147
|
await dispose();
|
|
1075
|
-
|
|
1148
|
+
process6.exit(code ?? 0);
|
|
1076
1149
|
}
|
|
1077
1150
|
function resolveKeyFromOpts(opts) {
|
|
1078
1151
|
if (opts.region !== void 0 && opts.org !== void 0 && opts.space !== void 0 && opts.app !== void 0) {
|
|
@@ -1088,7 +1161,7 @@ function resolveKeyFromOpts(opts) {
|
|
|
1088
1161
|
async function handleStop(opts) {
|
|
1089
1162
|
if (opts.all === true) {
|
|
1090
1163
|
const count = await stopAllDebuggers();
|
|
1091
|
-
|
|
1164
|
+
process6.stdout.write(`Stopped ${count.toString()} session(s).
|
|
1092
1165
|
`);
|
|
1093
1166
|
return;
|
|
1094
1167
|
}
|
|
@@ -1098,17 +1171,17 @@ async function handleStop(opts) {
|
|
|
1098
1171
|
...key === void 0 ? {} : { key }
|
|
1099
1172
|
});
|
|
1100
1173
|
if (result === void 0) {
|
|
1101
|
-
|
|
1102
|
-
|
|
1174
|
+
process6.stderr.write("No matching session found.\n");
|
|
1175
|
+
process6.exit(1);
|
|
1103
1176
|
}
|
|
1104
|
-
|
|
1177
|
+
process6.stdout.write(
|
|
1105
1178
|
`Stopped session ${result.sessionId} (${result.app}, port ${result.localPort.toString()}).
|
|
1106
1179
|
`
|
|
1107
1180
|
);
|
|
1108
1181
|
}
|
|
1109
1182
|
async function handleList() {
|
|
1110
1183
|
const sessions = await listSessions();
|
|
1111
|
-
|
|
1184
|
+
process6.stdout.write(`${JSON.stringify(sessions, null, 2)}
|
|
1112
1185
|
`);
|
|
1113
1186
|
}
|
|
1114
1187
|
async function handleStatus(opts) {
|
|
@@ -1118,7 +1191,7 @@ async function handleStatus(opts) {
|
|
|
1118
1191
|
space: opts.space,
|
|
1119
1192
|
app: opts.app
|
|
1120
1193
|
});
|
|
1121
|
-
|
|
1194
|
+
process6.stdout.write(`${JSON.stringify(session ?? null, null, 2)}
|
|
1122
1195
|
`);
|
|
1123
1196
|
}
|
|
1124
1197
|
async function main(argv) {
|
|
@@ -1139,22 +1212,22 @@ async function main(argv) {
|
|
|
1139
1212
|
await program.parseAsync([...argv]);
|
|
1140
1213
|
}
|
|
1141
1214
|
try {
|
|
1142
|
-
await main(
|
|
1215
|
+
await main(process6.argv);
|
|
1143
1216
|
} catch (err) {
|
|
1144
1217
|
if (err instanceof CfDebuggerError) {
|
|
1145
1218
|
if (err.code === "ABORTED") {
|
|
1146
|
-
|
|
1219
|
+
process6.stderr.write(`Aborted: ${err.message}
|
|
1147
1220
|
`);
|
|
1148
|
-
|
|
1221
|
+
process6.exit(130);
|
|
1149
1222
|
}
|
|
1150
|
-
|
|
1223
|
+
process6.stderr.write(`Error [${err.code}]: ${err.message}
|
|
1151
1224
|
`);
|
|
1152
1225
|
} else {
|
|
1153
1226
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1154
|
-
|
|
1227
|
+
process6.stderr.write(`Error: ${msg}
|
|
1155
1228
|
`);
|
|
1156
1229
|
}
|
|
1157
|
-
|
|
1230
|
+
process6.exit(1);
|
|
1158
1231
|
}
|
|
1159
1232
|
export {
|
|
1160
1233
|
main
|