@saptools/cf-debugger 0.1.4 → 0.1.5
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 +240 -186
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +205 -149
- 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,13 +22,12 @@ 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 CF_RESTART_TIMEOUT_MS = 12e4;
|
|
35
|
-
var CF_SSH_SIGNAL_TIMEOUT_MS = 15e3;
|
|
36
|
-
var CF_AUTH_MAX_ATTEMPTS = 3;
|
|
37
31
|
function buildEnv(cfHome) {
|
|
38
32
|
return { ...process.env, CF_HOME: cfHome };
|
|
39
33
|
}
|
|
@@ -58,6 +52,10 @@ async function runCf(args, context, timeoutMs = CF_CLI_TIMEOUT_MS) {
|
|
|
58
52
|
);
|
|
59
53
|
}
|
|
60
54
|
}
|
|
55
|
+
|
|
56
|
+
// src/cloud-foundry/commands.ts
|
|
57
|
+
var CF_RESTART_TIMEOUT_MS = 12e4;
|
|
58
|
+
var CF_AUTH_MAX_ATTEMPTS = 3;
|
|
61
59
|
async function cfApi(apiEndpoint, context) {
|
|
62
60
|
await runCf(["api", apiEndpoint], context);
|
|
63
61
|
}
|
|
@@ -123,6 +121,10 @@ async function cfEnableSsh(appName, context) {
|
|
|
123
121
|
async function cfRestartApp(appName, context) {
|
|
124
122
|
await runCf(["restart", appName], context, CF_RESTART_TIMEOUT_MS);
|
|
125
123
|
}
|
|
124
|
+
|
|
125
|
+
// src/cloud-foundry/ssh.ts
|
|
126
|
+
import { spawn } from "child_process";
|
|
127
|
+
var CF_SSH_SIGNAL_TIMEOUT_MS = 15e3;
|
|
126
128
|
async function cfSshOneShot(appName, command, context) {
|
|
127
129
|
return await new Promise((resolve) => {
|
|
128
130
|
const child = spawn(resolveBin(context), ["ssh", appName, "-c", command], {
|
|
@@ -197,7 +199,7 @@ function sessionCfHomeDir(sessionId) {
|
|
|
197
199
|
return join(saptoolsDir(), CF_DEBUGGER_HOMES_DIRNAME, sessionId);
|
|
198
200
|
}
|
|
199
201
|
|
|
200
|
-
// src/
|
|
202
|
+
// src/network/ports.ts
|
|
201
203
|
import { execFile as execFile2 } from "child_process";
|
|
202
204
|
import { createConnection, createServer } from "net";
|
|
203
205
|
import { promisify as promisify2 } from "util";
|
|
@@ -366,7 +368,7 @@ function resolveApiEndpoint(regionKey, override) {
|
|
|
366
368
|
return endpoint;
|
|
367
369
|
}
|
|
368
370
|
|
|
369
|
-
// src/state.ts
|
|
371
|
+
// src/session-state/store.ts
|
|
370
372
|
import { randomUUID } from "crypto";
|
|
371
373
|
import { mkdir as mkdir2, readFile, rename, writeFile } from "fs/promises";
|
|
372
374
|
import { hostname as getHostname } from "os";
|
|
@@ -421,7 +423,7 @@ async function withFileLock(lockPath, work, options) {
|
|
|
421
423
|
}
|
|
422
424
|
}
|
|
423
425
|
|
|
424
|
-
// src/state.ts
|
|
426
|
+
// src/session-state/store.ts
|
|
425
427
|
async function readJsonFile(path) {
|
|
426
428
|
let raw;
|
|
427
429
|
try {
|
|
@@ -653,13 +655,29 @@ async function removeSession(sessionId) {
|
|
|
653
655
|
});
|
|
654
656
|
}
|
|
655
657
|
|
|
656
|
-
// src/
|
|
658
|
+
// src/debug-session/constants.ts
|
|
657
659
|
var DEFAULT_TUNNEL_READY_TIMEOUT_MS = 3e4;
|
|
658
660
|
var POST_USR1_DELAY_MS = 300;
|
|
659
661
|
var PORT_CLEANUP_DELAY_MS = 600;
|
|
660
662
|
var CHILD_SIGTERM_GRACE_MS = 2e3;
|
|
661
663
|
var PORT_RECLAIM_DELAY_MS = 250;
|
|
662
664
|
var PID_TERMINATION_POLL_MS = 100;
|
|
665
|
+
|
|
666
|
+
// src/debug-session/orphans.ts
|
|
667
|
+
import { hostname as getHostname2 } from "os";
|
|
668
|
+
async function pruneAndCleanupOrphans() {
|
|
669
|
+
const result = await readAndPruneActiveSessions();
|
|
670
|
+
const host = getHostname2();
|
|
671
|
+
for (const removed of result.removed) {
|
|
672
|
+
if (removed.hostname === host) {
|
|
673
|
+
void killProcessOnPort(removed.localPort);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return result.sessions;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// src/debug-session/processes.ts
|
|
680
|
+
import process3 from "process";
|
|
663
681
|
function signalPidOrGroup(pid, signal) {
|
|
664
682
|
const isWindows = process3.platform === "win32";
|
|
665
683
|
if (!isWindows) {
|
|
@@ -699,24 +717,16 @@ async function killProcessGroupOrProc(child, timeoutMs = CHILD_SIGTERM_GRACE_MS)
|
|
|
699
717
|
}
|
|
700
718
|
await terminatePidOrGroup(child.pid, timeoutMs);
|
|
701
719
|
}
|
|
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
|
-
}
|
|
720
|
+
|
|
721
|
+
// src/debug-session/start.ts
|
|
712
722
|
function checkAbort(signal) {
|
|
713
723
|
if (signal?.aborted) {
|
|
714
724
|
throw new CfDebuggerError("ABORTED", "Operation aborted by caller");
|
|
715
725
|
}
|
|
716
726
|
}
|
|
717
727
|
function requireCredentials(options) {
|
|
718
|
-
const email = options.email ??
|
|
719
|
-
const password = options.password ??
|
|
728
|
+
const email = options.email ?? process4.env["SAP_EMAIL"];
|
|
729
|
+
const password = options.password ?? process4.env["SAP_PASSWORD"];
|
|
720
730
|
if (email === void 0 || email === "") {
|
|
721
731
|
throw new CfDebuggerError(
|
|
722
732
|
"MISSING_CREDENTIALS",
|
|
@@ -731,15 +741,7 @@ function requireCredentials(options) {
|
|
|
731
741
|
}
|
|
732
742
|
return { email, password };
|
|
733
743
|
}
|
|
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();
|
|
744
|
+
async function registerSession(options, apiEndpoint) {
|
|
743
745
|
const registration = await registerNewSession({
|
|
744
746
|
region: options.region,
|
|
745
747
|
org: options.org,
|
|
@@ -756,20 +758,151 @@ async function startDebugger(options) {
|
|
|
756
758
|
`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
759
|
);
|
|
758
760
|
}
|
|
759
|
-
|
|
761
|
+
return registration.session;
|
|
762
|
+
}
|
|
763
|
+
async function loginAndTarget(options, apiEndpoint, email, password, context, sessionId, emit) {
|
|
764
|
+
emit("logging-in");
|
|
765
|
+
await updateSessionStatus(sessionId, "logging-in");
|
|
766
|
+
await cfLogin(apiEndpoint, email, password, context);
|
|
767
|
+
checkAbort(options.signal);
|
|
768
|
+
emit("targeting");
|
|
769
|
+
await updateSessionStatus(sessionId, "targeting");
|
|
770
|
+
await cfTarget(options.org, options.space, context);
|
|
771
|
+
checkAbort(options.signal);
|
|
772
|
+
}
|
|
773
|
+
async function signalRemoteNode(options, context, sessionId, emit) {
|
|
774
|
+
emit("signaling");
|
|
775
|
+
await updateSessionStatus(sessionId, "signaling");
|
|
776
|
+
const signalResult = await cfSshOneShot(options.app, `kill -s USR1 $(pidof node)`, context);
|
|
777
|
+
if (!isSshDisabledError(signalResult.stderr)) {
|
|
778
|
+
if (signalResult.exitCode === 0) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const detail = signalResult.stderr.trim().length > 0 ? signalResult.stderr.trim() : `exit code ${String(signalResult.exitCode)}`;
|
|
782
|
+
throw new CfDebuggerError(
|
|
783
|
+
"USR1_SIGNAL_FAILED",
|
|
784
|
+
`Failed to send SIGUSR1 to the Node.js process on ${options.app}: ${detail}`,
|
|
785
|
+
signalResult.stderr
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
const alreadyEnabled = await cfSshEnabled(options.app, context);
|
|
789
|
+
if (!alreadyEnabled) {
|
|
790
|
+
emit("ssh-enabling", "Enabling SSH on the app");
|
|
791
|
+
await updateSessionStatus(sessionId, "ssh-enabling");
|
|
792
|
+
await cfEnableSsh(options.app, context);
|
|
793
|
+
}
|
|
794
|
+
emit("ssh-restarting", "Restarting app so SSH becomes active");
|
|
795
|
+
await updateSessionStatus(sessionId, "ssh-restarting");
|
|
796
|
+
await cfRestartApp(options.app, context);
|
|
797
|
+
checkAbort(options.signal);
|
|
798
|
+
await retryRemoteSignal(options, context, sessionId, emit);
|
|
799
|
+
}
|
|
800
|
+
async function retryRemoteSignal(options, context, sessionId, emit) {
|
|
801
|
+
emit("signaling");
|
|
802
|
+
await updateSessionStatus(sessionId, "signaling");
|
|
803
|
+
const retrySignalResult = await cfSshOneShot(
|
|
804
|
+
options.app,
|
|
805
|
+
`kill -s USR1 $(pidof node)`,
|
|
806
|
+
context
|
|
807
|
+
);
|
|
808
|
+
if (retrySignalResult.exitCode === 0) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
const detail = retrySignalResult.stderr.trim().length > 0 ? retrySignalResult.stderr.trim() : `exit code ${String(retrySignalResult.exitCode)}`;
|
|
812
|
+
throw new CfDebuggerError(
|
|
813
|
+
"USR1_SIGNAL_FAILED",
|
|
814
|
+
`Failed to send SIGUSR1 to the Node.js process on ${options.app} after enabling SSH: ${detail}`,
|
|
815
|
+
retrySignalResult.stderr
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
async function waitAfterSignal(signal) {
|
|
819
|
+
await new Promise((resolve) => {
|
|
820
|
+
setTimeout(resolve, POST_USR1_DELAY_MS);
|
|
821
|
+
});
|
|
822
|
+
checkAbort(signal);
|
|
823
|
+
}
|
|
824
|
+
async function ensurePortAvailable(localPort) {
|
|
825
|
+
if (await isPortFree(localPort)) {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
await killProcessOnPort(localPort);
|
|
829
|
+
await new Promise((resolve) => {
|
|
830
|
+
setTimeout(resolve, PORT_RECLAIM_DELAY_MS);
|
|
831
|
+
});
|
|
832
|
+
if (!await isPortFree(localPort)) {
|
|
833
|
+
throw new CfDebuggerError(
|
|
834
|
+
"PORT_UNAVAILABLE",
|
|
835
|
+
`Local port ${localPort.toString()} is in use and could not be reclaimed for the tunnel.`
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
async function openReadyTunnel(options, session, context, tunnelReadyTimeoutMs, onChild) {
|
|
840
|
+
await ensurePortAvailable(session.localPort);
|
|
841
|
+
const child = spawnSshTunnel(options.app, session.localPort, session.remotePort, context);
|
|
842
|
+
onChild(child);
|
|
843
|
+
if (child.pid !== void 0) {
|
|
844
|
+
await updateSessionPid(session.sessionId, child.pid);
|
|
845
|
+
}
|
|
846
|
+
const ready = await probeTunnelReady(session.localPort, tunnelReadyTimeoutMs);
|
|
847
|
+
checkAbort(options.signal);
|
|
848
|
+
if (!ready) {
|
|
849
|
+
throw new CfDebuggerError(
|
|
850
|
+
"TUNNEL_NOT_READY",
|
|
851
|
+
`SSH tunnel on port ${session.localPort.toString()} did not become ready within ${Math.round(tunnelReadyTimeoutMs / 1e3).toString()}s.`
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
const listeningPid = await findListeningProcessId(session.localPort);
|
|
855
|
+
const activePid = listeningPid ?? child.pid ?? session.pid;
|
|
856
|
+
if (activePid !== session.pid) {
|
|
857
|
+
await updateSessionPid(session.sessionId, activePid);
|
|
858
|
+
}
|
|
859
|
+
return { child, activePid };
|
|
860
|
+
}
|
|
861
|
+
function attachTunnelEvents(child, markClosed, resolveExit, emit) {
|
|
862
|
+
child.on("close", (code) => {
|
|
863
|
+
markClosed();
|
|
864
|
+
resolveExit(code);
|
|
865
|
+
});
|
|
866
|
+
child.on("error", (err) => {
|
|
867
|
+
emit("error", err.message);
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
function createHandle(session, emit, finalize, exitPromise) {
|
|
871
|
+
let disposePromise;
|
|
872
|
+
return {
|
|
873
|
+
session,
|
|
874
|
+
dispose: async () => {
|
|
875
|
+
disposePromise ??= (async () => {
|
|
876
|
+
emit("stopping");
|
|
877
|
+
await updateSessionStatus(session.sessionId, "stopping");
|
|
878
|
+
await finalize();
|
|
879
|
+
})();
|
|
880
|
+
await disposePromise;
|
|
881
|
+
},
|
|
882
|
+
waitForExit: async () => {
|
|
883
|
+
return await exitPromise;
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
async function startDebugger(options) {
|
|
888
|
+
const { email, password } = requireCredentials(options);
|
|
889
|
+
const apiEndpoint = resolveApiEndpoint(options.region, options.apiEndpoint);
|
|
890
|
+
const tunnelReadyTimeoutMs = options.tunnelReadyTimeoutMs ?? DEFAULT_TUNNEL_READY_TIMEOUT_MS;
|
|
891
|
+
const emit = (status, message) => {
|
|
892
|
+
options.onStatus?.(status, message);
|
|
893
|
+
};
|
|
894
|
+
checkAbort(options.signal);
|
|
895
|
+
await pruneAndCleanupOrphans();
|
|
896
|
+
const session = await registerSession(options, apiEndpoint);
|
|
760
897
|
const context = { cfHome: session.cfHomeDir };
|
|
761
898
|
let child;
|
|
762
899
|
let tunnelClosed = false;
|
|
763
|
-
let exitResolve
|
|
900
|
+
let exitResolve = (_code) => {
|
|
901
|
+
throw new Error("Exit resolver was used before initialization");
|
|
902
|
+
};
|
|
764
903
|
const exitPromise = new Promise((resolve) => {
|
|
765
904
|
exitResolve = resolve;
|
|
766
905
|
});
|
|
767
|
-
const cleanupFilesystem = async () => {
|
|
768
|
-
try {
|
|
769
|
-
await rm(session.cfHomeDir, { recursive: true, force: true });
|
|
770
|
-
} catch {
|
|
771
|
-
}
|
|
772
|
-
};
|
|
773
906
|
const finalize = async () => {
|
|
774
907
|
if (!tunnelClosed) {
|
|
775
908
|
tunnelClosed = true;
|
|
@@ -781,125 +914,36 @@ async function startDebugger(options) {
|
|
|
781
914
|
}, PORT_CLEANUP_DELAY_MS);
|
|
782
915
|
}
|
|
783
916
|
await removeSession(session.sessionId);
|
|
784
|
-
await cleanupFilesystem();
|
|
917
|
+
await cleanupFilesystem(session.cfHomeDir);
|
|
785
918
|
emit("stopped");
|
|
786
919
|
};
|
|
787
920
|
try {
|
|
788
921
|
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);
|
|
922
|
+
await loginAndTarget(options, apiEndpoint, email, password, context, session.sessionId, emit);
|
|
797
923
|
await killProcessOnPort(session.localPort);
|
|
798
924
|
await new Promise((resolve) => {
|
|
799
925
|
setTimeout(resolve, 200);
|
|
800
926
|
});
|
|
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);
|
|
927
|
+
await signalRemoteNode(options, context, session.sessionId, emit);
|
|
928
|
+
await waitAfterSignal(options.signal);
|
|
846
929
|
emit("tunneling");
|
|
847
930
|
await updateSessionStatus(session.sessionId, "tunneling");
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
);
|
|
931
|
+
const tunnel = await openReadyTunnel(
|
|
932
|
+
options,
|
|
933
|
+
session,
|
|
934
|
+
context,
|
|
935
|
+
tunnelReadyTimeoutMs,
|
|
936
|
+
(tunnelChild) => {
|
|
937
|
+
attachTunnelEvents(tunnelChild, () => {
|
|
938
|
+
tunnelClosed = true;
|
|
939
|
+
}, exitResolve, emit);
|
|
858
940
|
}
|
|
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
|
-
}
|
|
941
|
+
);
|
|
942
|
+
child = tunnel.child;
|
|
884
943
|
emit("ready");
|
|
885
944
|
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;
|
|
945
|
+
const activeSession = readySession ?? { ...session, pid: tunnel.activePid, status: "ready" };
|
|
946
|
+
return createHandle(activeSession, emit, finalize, exitPromise);
|
|
903
947
|
} catch (err) {
|
|
904
948
|
const message = err instanceof Error ? err.message : String(err);
|
|
905
949
|
emit("error", message);
|
|
@@ -907,6 +951,16 @@ async function startDebugger(options) {
|
|
|
907
951
|
throw err;
|
|
908
952
|
}
|
|
909
953
|
}
|
|
954
|
+
async function cleanupFilesystem(cfHomeDir) {
|
|
955
|
+
try {
|
|
956
|
+
await rm(cfHomeDir, { recursive: true, force: true });
|
|
957
|
+
} catch {
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// src/debug-session/sessions.ts
|
|
962
|
+
import { rm as rm2 } from "fs/promises";
|
|
963
|
+
import process5 from "process";
|
|
910
964
|
async function stopDebugger(options) {
|
|
911
965
|
const sessions = await pruneAndCleanupOrphans();
|
|
912
966
|
let target;
|
|
@@ -919,7 +973,7 @@ async function stopDebugger(options) {
|
|
|
919
973
|
if (target === void 0) {
|
|
920
974
|
return void 0;
|
|
921
975
|
}
|
|
922
|
-
if (target.pid !==
|
|
976
|
+
if (target.pid !== process5.pid) {
|
|
923
977
|
try {
|
|
924
978
|
await terminatePidOrGroup(target.pid);
|
|
925
979
|
} catch {
|
|
@@ -930,7 +984,7 @@ async function stopDebugger(options) {
|
|
|
930
984
|
}, PORT_CLEANUP_DELAY_MS);
|
|
931
985
|
const removed = await removeSession(target.sessionId);
|
|
932
986
|
try {
|
|
933
|
-
await
|
|
987
|
+
await rm2(target.cfHomeDir, { recursive: true, force: true });
|
|
934
988
|
} catch {
|
|
935
989
|
}
|
|
936
990
|
return removed ?? target;
|
|
@@ -957,9 +1011,9 @@ async function getSession(key) {
|
|
|
957
1011
|
// src/cli.ts
|
|
958
1012
|
function readRequiredOption(value, flag) {
|
|
959
1013
|
if (value === void 0 || value === "") {
|
|
960
|
-
|
|
1014
|
+
process6.stderr.write(`Missing required option ${flag}
|
|
961
1015
|
`);
|
|
962
|
-
|
|
1016
|
+
process6.exit(1);
|
|
963
1017
|
}
|
|
964
1018
|
return value;
|
|
965
1019
|
}
|
|
@@ -969,9 +1023,9 @@ function parseOptionalPort(raw) {
|
|
|
969
1023
|
}
|
|
970
1024
|
const port = Number.parseInt(raw, 10);
|
|
971
1025
|
if (Number.isNaN(port) || port <= 0 || port > 65535) {
|
|
972
|
-
|
|
1026
|
+
process6.stderr.write(`Invalid port: ${raw}
|
|
973
1027
|
`);
|
|
974
|
-
|
|
1028
|
+
process6.exit(1);
|
|
975
1029
|
}
|
|
976
1030
|
return port;
|
|
977
1031
|
}
|
|
@@ -981,16 +1035,16 @@ function parseOptionalTimeout(raw) {
|
|
|
981
1035
|
}
|
|
982
1036
|
const seconds = Number.parseInt(raw, 10);
|
|
983
1037
|
if (Number.isNaN(seconds) || seconds <= 0) {
|
|
984
|
-
|
|
1038
|
+
process6.stderr.write(`Invalid timeout: ${raw}
|
|
985
1039
|
`);
|
|
986
|
-
|
|
1040
|
+
process6.exit(1);
|
|
987
1041
|
}
|
|
988
1042
|
return seconds * 1e3;
|
|
989
1043
|
}
|
|
990
1044
|
function logStatus(verbose, status, message) {
|
|
991
1045
|
if (verbose) {
|
|
992
1046
|
const suffix = message === void 0 ? "" : `: ${message}`;
|
|
993
|
-
|
|
1047
|
+
process6.stdout.write(`[cf-debugger] ${status}${suffix}
|
|
994
1048
|
`);
|
|
995
1049
|
}
|
|
996
1050
|
}
|
|
@@ -1005,17 +1059,17 @@ async function handleStart(opts) {
|
|
|
1005
1059
|
const abortController = new AbortController();
|
|
1006
1060
|
const onStartupSignal = (exitCode) => () => {
|
|
1007
1061
|
abortController.abort();
|
|
1008
|
-
|
|
1062
|
+
process6.stderr.write(`
|
|
1009
1063
|
Aborting startup for ${app}...
|
|
1010
1064
|
`);
|
|
1011
1065
|
setTimeout(() => {
|
|
1012
|
-
|
|
1066
|
+
process6.exit(exitCode);
|
|
1013
1067
|
}, 5e3).unref();
|
|
1014
1068
|
};
|
|
1015
1069
|
const startupSigint = onStartupSignal(130);
|
|
1016
1070
|
const startupSigterm = onStartupSignal(143);
|
|
1017
|
-
|
|
1018
|
-
|
|
1071
|
+
process6.on("SIGINT", startupSigint);
|
|
1072
|
+
process6.on("SIGTERM", startupSigterm);
|
|
1019
1073
|
let handle;
|
|
1020
1074
|
try {
|
|
1021
1075
|
handle = await startDebugger({
|
|
@@ -1032,10 +1086,10 @@ Aborting startup for ${app}...
|
|
|
1032
1086
|
}
|
|
1033
1087
|
});
|
|
1034
1088
|
} finally {
|
|
1035
|
-
|
|
1036
|
-
|
|
1089
|
+
process6.off("SIGINT", startupSigint);
|
|
1090
|
+
process6.off("SIGTERM", startupSigterm);
|
|
1037
1091
|
}
|
|
1038
|
-
|
|
1092
|
+
process6.stdout.write(
|
|
1039
1093
|
`Debugger ready for ${app} (${region}/${org}/${space}).
|
|
1040
1094
|
Local port: ${handle.session.localPort.toString()}
|
|
1041
1095
|
Remote port: ${handle.session.remotePort.toString()}
|
|
@@ -1047,32 +1101,32 @@ Press Ctrl+C to stop.
|
|
|
1047
1101
|
let disposePromise;
|
|
1048
1102
|
const dispose = async () => {
|
|
1049
1103
|
disposePromise ??= (async () => {
|
|
1050
|
-
|
|
1104
|
+
process6.stdout.write(`
|
|
1051
1105
|
Stopping debugger for ${app}...
|
|
1052
1106
|
`);
|
|
1053
1107
|
try {
|
|
1054
1108
|
await handle.dispose();
|
|
1055
1109
|
} catch (err) {
|
|
1056
1110
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1057
|
-
|
|
1111
|
+
process6.stderr.write(`Error during stop: ${msg}
|
|
1058
1112
|
`);
|
|
1059
1113
|
}
|
|
1060
1114
|
})();
|
|
1061
1115
|
await disposePromise;
|
|
1062
1116
|
};
|
|
1063
|
-
|
|
1117
|
+
process6.on("SIGINT", () => {
|
|
1064
1118
|
void dispose().then(() => {
|
|
1065
|
-
|
|
1119
|
+
process6.exit(130);
|
|
1066
1120
|
});
|
|
1067
1121
|
});
|
|
1068
|
-
|
|
1122
|
+
process6.on("SIGTERM", () => {
|
|
1069
1123
|
void dispose().then(() => {
|
|
1070
|
-
|
|
1124
|
+
process6.exit(143);
|
|
1071
1125
|
});
|
|
1072
1126
|
});
|
|
1073
1127
|
const code = await handle.waitForExit();
|
|
1074
1128
|
await dispose();
|
|
1075
|
-
|
|
1129
|
+
process6.exit(code ?? 0);
|
|
1076
1130
|
}
|
|
1077
1131
|
function resolveKeyFromOpts(opts) {
|
|
1078
1132
|
if (opts.region !== void 0 && opts.org !== void 0 && opts.space !== void 0 && opts.app !== void 0) {
|
|
@@ -1088,7 +1142,7 @@ function resolveKeyFromOpts(opts) {
|
|
|
1088
1142
|
async function handleStop(opts) {
|
|
1089
1143
|
if (opts.all === true) {
|
|
1090
1144
|
const count = await stopAllDebuggers();
|
|
1091
|
-
|
|
1145
|
+
process6.stdout.write(`Stopped ${count.toString()} session(s).
|
|
1092
1146
|
`);
|
|
1093
1147
|
return;
|
|
1094
1148
|
}
|
|
@@ -1098,17 +1152,17 @@ async function handleStop(opts) {
|
|
|
1098
1152
|
...key === void 0 ? {} : { key }
|
|
1099
1153
|
});
|
|
1100
1154
|
if (result === void 0) {
|
|
1101
|
-
|
|
1102
|
-
|
|
1155
|
+
process6.stderr.write("No matching session found.\n");
|
|
1156
|
+
process6.exit(1);
|
|
1103
1157
|
}
|
|
1104
|
-
|
|
1158
|
+
process6.stdout.write(
|
|
1105
1159
|
`Stopped session ${result.sessionId} (${result.app}, port ${result.localPort.toString()}).
|
|
1106
1160
|
`
|
|
1107
1161
|
);
|
|
1108
1162
|
}
|
|
1109
1163
|
async function handleList() {
|
|
1110
1164
|
const sessions = await listSessions();
|
|
1111
|
-
|
|
1165
|
+
process6.stdout.write(`${JSON.stringify(sessions, null, 2)}
|
|
1112
1166
|
`);
|
|
1113
1167
|
}
|
|
1114
1168
|
async function handleStatus(opts) {
|
|
@@ -1118,7 +1172,7 @@ async function handleStatus(opts) {
|
|
|
1118
1172
|
space: opts.space,
|
|
1119
1173
|
app: opts.app
|
|
1120
1174
|
});
|
|
1121
|
-
|
|
1175
|
+
process6.stdout.write(`${JSON.stringify(session ?? null, null, 2)}
|
|
1122
1176
|
`);
|
|
1123
1177
|
}
|
|
1124
1178
|
async function main(argv) {
|
|
@@ -1139,22 +1193,22 @@ async function main(argv) {
|
|
|
1139
1193
|
await program.parseAsync([...argv]);
|
|
1140
1194
|
}
|
|
1141
1195
|
try {
|
|
1142
|
-
await main(
|
|
1196
|
+
await main(process6.argv);
|
|
1143
1197
|
} catch (err) {
|
|
1144
1198
|
if (err instanceof CfDebuggerError) {
|
|
1145
1199
|
if (err.code === "ABORTED") {
|
|
1146
|
-
|
|
1200
|
+
process6.stderr.write(`Aborted: ${err.message}
|
|
1147
1201
|
`);
|
|
1148
|
-
|
|
1202
|
+
process6.exit(130);
|
|
1149
1203
|
}
|
|
1150
|
-
|
|
1204
|
+
process6.stderr.write(`Error [${err.code}]: ${err.message}
|
|
1151
1205
|
`);
|
|
1152
1206
|
} else {
|
|
1153
1207
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1154
|
-
|
|
1208
|
+
process6.stderr.write(`Error: ${msg}
|
|
1155
1209
|
`);
|
|
1156
1210
|
}
|
|
1157
|
-
|
|
1211
|
+
process6.exit(1);
|
|
1158
1212
|
}
|
|
1159
1213
|
export {
|
|
1160
1214
|
main
|