@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/index.js
CHANGED
|
@@ -14,20 +14,16 @@ var CfDebuggerError = class extends Error {
|
|
|
14
14
|
}
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
// src/
|
|
17
|
+
// src/debug-session/start.ts
|
|
18
18
|
import { mkdir as mkdir3, rm } from "fs/promises";
|
|
19
|
-
import
|
|
20
|
-
import process3 from "process";
|
|
19
|
+
import process4 from "process";
|
|
21
20
|
|
|
22
|
-
// src/
|
|
23
|
-
import { execFile
|
|
21
|
+
// src/cloud-foundry/execute.ts
|
|
22
|
+
import { execFile } from "child_process";
|
|
24
23
|
import { promisify } from "util";
|
|
25
24
|
var execFileAsync = promisify(execFile);
|
|
26
25
|
var MAX_BUFFER = 16 * 1024 * 1024;
|
|
27
26
|
var CF_CLI_TIMEOUT_MS = 3e4;
|
|
28
|
-
var CF_RESTART_TIMEOUT_MS = 12e4;
|
|
29
|
-
var CF_SSH_SIGNAL_TIMEOUT_MS = 15e3;
|
|
30
|
-
var CF_AUTH_MAX_ATTEMPTS = 3;
|
|
31
27
|
function buildEnv(cfHome) {
|
|
32
28
|
return { ...process.env, CF_HOME: cfHome };
|
|
33
29
|
}
|
|
@@ -52,6 +48,10 @@ async function runCf(args, context, timeoutMs = CF_CLI_TIMEOUT_MS) {
|
|
|
52
48
|
);
|
|
53
49
|
}
|
|
54
50
|
}
|
|
51
|
+
|
|
52
|
+
// src/cloud-foundry/commands.ts
|
|
53
|
+
var CF_RESTART_TIMEOUT_MS = 12e4;
|
|
54
|
+
var CF_AUTH_MAX_ATTEMPTS = 3;
|
|
55
55
|
async function cfApi(apiEndpoint, context) {
|
|
56
56
|
await runCf(["api", apiEndpoint], context);
|
|
57
57
|
}
|
|
@@ -117,6 +117,10 @@ async function cfEnableSsh(appName, context) {
|
|
|
117
117
|
async function cfRestartApp(appName, context) {
|
|
118
118
|
await runCf(["restart", appName], context, CF_RESTART_TIMEOUT_MS);
|
|
119
119
|
}
|
|
120
|
+
|
|
121
|
+
// src/cloud-foundry/ssh.ts
|
|
122
|
+
import { spawn } from "child_process";
|
|
123
|
+
var CF_SSH_SIGNAL_TIMEOUT_MS = 15e3;
|
|
120
124
|
async function cfSshOneShot(appName, command, context) {
|
|
121
125
|
return await new Promise((resolve) => {
|
|
122
126
|
const child = spawn(resolveBin(context), ["ssh", appName, "-c", command], {
|
|
@@ -191,7 +195,7 @@ function sessionCfHomeDir(sessionId) {
|
|
|
191
195
|
return join(saptoolsDir(), CF_DEBUGGER_HOMES_DIRNAME, sessionId);
|
|
192
196
|
}
|
|
193
197
|
|
|
194
|
-
// src/
|
|
198
|
+
// src/network/ports.ts
|
|
195
199
|
import { execFile as execFile2 } from "child_process";
|
|
196
200
|
import { createConnection, createServer } from "net";
|
|
197
201
|
import { promisify as promisify2 } from "util";
|
|
@@ -363,7 +367,7 @@ function listKnownRegionKeys() {
|
|
|
363
367
|
return Object.keys(REGION_API_ENDPOINTS);
|
|
364
368
|
}
|
|
365
369
|
|
|
366
|
-
// src/state.ts
|
|
370
|
+
// src/session-state/store.ts
|
|
367
371
|
import { randomUUID } from "crypto";
|
|
368
372
|
import { mkdir as mkdir2, readFile, rename, writeFile } from "fs/promises";
|
|
369
373
|
import { hostname as getHostname } from "os";
|
|
@@ -418,7 +422,7 @@ async function withFileLock(lockPath, work, options) {
|
|
|
418
422
|
}
|
|
419
423
|
}
|
|
420
424
|
|
|
421
|
-
// src/state.ts
|
|
425
|
+
// src/session-state/store.ts
|
|
422
426
|
async function readJsonFile(path) {
|
|
423
427
|
let raw;
|
|
424
428
|
try {
|
|
@@ -650,13 +654,29 @@ async function removeSession(sessionId) {
|
|
|
650
654
|
});
|
|
651
655
|
}
|
|
652
656
|
|
|
653
|
-
// src/
|
|
657
|
+
// src/debug-session/constants.ts
|
|
654
658
|
var DEFAULT_TUNNEL_READY_TIMEOUT_MS = 3e4;
|
|
655
659
|
var POST_USR1_DELAY_MS = 300;
|
|
656
660
|
var PORT_CLEANUP_DELAY_MS = 600;
|
|
657
661
|
var CHILD_SIGTERM_GRACE_MS = 2e3;
|
|
658
662
|
var PORT_RECLAIM_DELAY_MS = 250;
|
|
659
663
|
var PID_TERMINATION_POLL_MS = 100;
|
|
664
|
+
|
|
665
|
+
// src/debug-session/orphans.ts
|
|
666
|
+
import { hostname as getHostname2 } from "os";
|
|
667
|
+
async function pruneAndCleanupOrphans() {
|
|
668
|
+
const result = await readAndPruneActiveSessions();
|
|
669
|
+
const host = getHostname2();
|
|
670
|
+
for (const removed of result.removed) {
|
|
671
|
+
if (removed.hostname === host) {
|
|
672
|
+
void killProcessOnPort(removed.localPort);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return result.sessions;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// src/debug-session/processes.ts
|
|
679
|
+
import process3 from "process";
|
|
660
680
|
function signalPidOrGroup(pid, signal) {
|
|
661
681
|
const isWindows = process3.platform === "win32";
|
|
662
682
|
if (!isWindows) {
|
|
@@ -696,24 +716,16 @@ async function killProcessGroupOrProc(child, timeoutMs = CHILD_SIGTERM_GRACE_MS)
|
|
|
696
716
|
}
|
|
697
717
|
await terminatePidOrGroup(child.pid, timeoutMs);
|
|
698
718
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
const host = getHostname2();
|
|
702
|
-
for (const removed of result.removed) {
|
|
703
|
-
if (removed.hostname === host) {
|
|
704
|
-
void killProcessOnPort(removed.localPort);
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
return result.sessions;
|
|
708
|
-
}
|
|
719
|
+
|
|
720
|
+
// src/debug-session/start.ts
|
|
709
721
|
function checkAbort(signal) {
|
|
710
722
|
if (signal?.aborted) {
|
|
711
723
|
throw new CfDebuggerError("ABORTED", "Operation aborted by caller");
|
|
712
724
|
}
|
|
713
725
|
}
|
|
714
726
|
function requireCredentials(options) {
|
|
715
|
-
const email = options.email ??
|
|
716
|
-
const password = options.password ??
|
|
727
|
+
const email = options.email ?? process4.env["SAP_EMAIL"];
|
|
728
|
+
const password = options.password ?? process4.env["SAP_PASSWORD"];
|
|
717
729
|
if (email === void 0 || email === "") {
|
|
718
730
|
throw new CfDebuggerError(
|
|
719
731
|
"MISSING_CREDENTIALS",
|
|
@@ -728,15 +740,7 @@ function requireCredentials(options) {
|
|
|
728
740
|
}
|
|
729
741
|
return { email, password };
|
|
730
742
|
}
|
|
731
|
-
async function
|
|
732
|
-
const { email, password } = requireCredentials(options);
|
|
733
|
-
const apiEndpoint = resolveApiEndpoint(options.region, options.apiEndpoint);
|
|
734
|
-
const tunnelReadyTimeoutMs = options.tunnelReadyTimeoutMs ?? DEFAULT_TUNNEL_READY_TIMEOUT_MS;
|
|
735
|
-
const emit = (status, message) => {
|
|
736
|
-
options.onStatus?.(status, message);
|
|
737
|
-
};
|
|
738
|
-
checkAbort(options.signal);
|
|
739
|
-
await pruneAndCleanupOrphans();
|
|
743
|
+
async function registerSession(options, apiEndpoint) {
|
|
740
744
|
const registration = await registerNewSession({
|
|
741
745
|
region: options.region,
|
|
742
746
|
org: options.org,
|
|
@@ -753,20 +757,151 @@ async function startDebugger(options) {
|
|
|
753
757
|
`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\`.`
|
|
754
758
|
);
|
|
755
759
|
}
|
|
756
|
-
|
|
760
|
+
return registration.session;
|
|
761
|
+
}
|
|
762
|
+
async function loginAndTarget(options, apiEndpoint, email, password, context, sessionId, emit) {
|
|
763
|
+
emit("logging-in");
|
|
764
|
+
await updateSessionStatus(sessionId, "logging-in");
|
|
765
|
+
await cfLogin(apiEndpoint, email, password, context);
|
|
766
|
+
checkAbort(options.signal);
|
|
767
|
+
emit("targeting");
|
|
768
|
+
await updateSessionStatus(sessionId, "targeting");
|
|
769
|
+
await cfTarget(options.org, options.space, context);
|
|
770
|
+
checkAbort(options.signal);
|
|
771
|
+
}
|
|
772
|
+
async function signalRemoteNode(options, context, sessionId, emit) {
|
|
773
|
+
emit("signaling");
|
|
774
|
+
await updateSessionStatus(sessionId, "signaling");
|
|
775
|
+
const signalResult = await cfSshOneShot(options.app, `kill -s USR1 $(pidof node)`, context);
|
|
776
|
+
if (!isSshDisabledError(signalResult.stderr)) {
|
|
777
|
+
if (signalResult.exitCode === 0) {
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
const detail = signalResult.stderr.trim().length > 0 ? signalResult.stderr.trim() : `exit code ${String(signalResult.exitCode)}`;
|
|
781
|
+
throw new CfDebuggerError(
|
|
782
|
+
"USR1_SIGNAL_FAILED",
|
|
783
|
+
`Failed to send SIGUSR1 to the Node.js process on ${options.app}: ${detail}`,
|
|
784
|
+
signalResult.stderr
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
const alreadyEnabled = await cfSshEnabled(options.app, context);
|
|
788
|
+
if (!alreadyEnabled) {
|
|
789
|
+
emit("ssh-enabling", "Enabling SSH on the app");
|
|
790
|
+
await updateSessionStatus(sessionId, "ssh-enabling");
|
|
791
|
+
await cfEnableSsh(options.app, context);
|
|
792
|
+
}
|
|
793
|
+
emit("ssh-restarting", "Restarting app so SSH becomes active");
|
|
794
|
+
await updateSessionStatus(sessionId, "ssh-restarting");
|
|
795
|
+
await cfRestartApp(options.app, context);
|
|
796
|
+
checkAbort(options.signal);
|
|
797
|
+
await retryRemoteSignal(options, context, sessionId, emit);
|
|
798
|
+
}
|
|
799
|
+
async function retryRemoteSignal(options, context, sessionId, emit) {
|
|
800
|
+
emit("signaling");
|
|
801
|
+
await updateSessionStatus(sessionId, "signaling");
|
|
802
|
+
const retrySignalResult = await cfSshOneShot(
|
|
803
|
+
options.app,
|
|
804
|
+
`kill -s USR1 $(pidof node)`,
|
|
805
|
+
context
|
|
806
|
+
);
|
|
807
|
+
if (retrySignalResult.exitCode === 0) {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const detail = retrySignalResult.stderr.trim().length > 0 ? retrySignalResult.stderr.trim() : `exit code ${String(retrySignalResult.exitCode)}`;
|
|
811
|
+
throw new CfDebuggerError(
|
|
812
|
+
"USR1_SIGNAL_FAILED",
|
|
813
|
+
`Failed to send SIGUSR1 to the Node.js process on ${options.app} after enabling SSH: ${detail}`,
|
|
814
|
+
retrySignalResult.stderr
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
async function waitAfterSignal(signal) {
|
|
818
|
+
await new Promise((resolve) => {
|
|
819
|
+
setTimeout(resolve, POST_USR1_DELAY_MS);
|
|
820
|
+
});
|
|
821
|
+
checkAbort(signal);
|
|
822
|
+
}
|
|
823
|
+
async function ensurePortAvailable(localPort) {
|
|
824
|
+
if (await isPortFree(localPort)) {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
await killProcessOnPort(localPort);
|
|
828
|
+
await new Promise((resolve) => {
|
|
829
|
+
setTimeout(resolve, PORT_RECLAIM_DELAY_MS);
|
|
830
|
+
});
|
|
831
|
+
if (!await isPortFree(localPort)) {
|
|
832
|
+
throw new CfDebuggerError(
|
|
833
|
+
"PORT_UNAVAILABLE",
|
|
834
|
+
`Local port ${localPort.toString()} is in use and could not be reclaimed for the tunnel.`
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
async function openReadyTunnel(options, session, context, tunnelReadyTimeoutMs, onChild) {
|
|
839
|
+
await ensurePortAvailable(session.localPort);
|
|
840
|
+
const child = spawnSshTunnel(options.app, session.localPort, session.remotePort, context);
|
|
841
|
+
onChild(child);
|
|
842
|
+
if (child.pid !== void 0) {
|
|
843
|
+
await updateSessionPid(session.sessionId, child.pid);
|
|
844
|
+
}
|
|
845
|
+
const ready = await probeTunnelReady(session.localPort, tunnelReadyTimeoutMs);
|
|
846
|
+
checkAbort(options.signal);
|
|
847
|
+
if (!ready) {
|
|
848
|
+
throw new CfDebuggerError(
|
|
849
|
+
"TUNNEL_NOT_READY",
|
|
850
|
+
`SSH tunnel on port ${session.localPort.toString()} did not become ready within ${Math.round(tunnelReadyTimeoutMs / 1e3).toString()}s.`
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
const listeningPid = await findListeningProcessId(session.localPort);
|
|
854
|
+
const activePid = listeningPid ?? child.pid ?? session.pid;
|
|
855
|
+
if (activePid !== session.pid) {
|
|
856
|
+
await updateSessionPid(session.sessionId, activePid);
|
|
857
|
+
}
|
|
858
|
+
return { child, activePid };
|
|
859
|
+
}
|
|
860
|
+
function attachTunnelEvents(child, markClosed, resolveExit, emit) {
|
|
861
|
+
child.on("close", (code) => {
|
|
862
|
+
markClosed();
|
|
863
|
+
resolveExit(code);
|
|
864
|
+
});
|
|
865
|
+
child.on("error", (err) => {
|
|
866
|
+
emit("error", err.message);
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
function createHandle(session, emit, finalize, exitPromise) {
|
|
870
|
+
let disposePromise;
|
|
871
|
+
return {
|
|
872
|
+
session,
|
|
873
|
+
dispose: async () => {
|
|
874
|
+
disposePromise ??= (async () => {
|
|
875
|
+
emit("stopping");
|
|
876
|
+
await updateSessionStatus(session.sessionId, "stopping");
|
|
877
|
+
await finalize();
|
|
878
|
+
})();
|
|
879
|
+
await disposePromise;
|
|
880
|
+
},
|
|
881
|
+
waitForExit: async () => {
|
|
882
|
+
return await exitPromise;
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
async function startDebugger(options) {
|
|
887
|
+
const { email, password } = requireCredentials(options);
|
|
888
|
+
const apiEndpoint = resolveApiEndpoint(options.region, options.apiEndpoint);
|
|
889
|
+
const tunnelReadyTimeoutMs = options.tunnelReadyTimeoutMs ?? DEFAULT_TUNNEL_READY_TIMEOUT_MS;
|
|
890
|
+
const emit = (status, message) => {
|
|
891
|
+
options.onStatus?.(status, message);
|
|
892
|
+
};
|
|
893
|
+
checkAbort(options.signal);
|
|
894
|
+
await pruneAndCleanupOrphans();
|
|
895
|
+
const session = await registerSession(options, apiEndpoint);
|
|
757
896
|
const context = { cfHome: session.cfHomeDir };
|
|
758
897
|
let child;
|
|
759
898
|
let tunnelClosed = false;
|
|
760
|
-
let exitResolve
|
|
899
|
+
let exitResolve = (_code) => {
|
|
900
|
+
throw new Error("Exit resolver was used before initialization");
|
|
901
|
+
};
|
|
761
902
|
const exitPromise = new Promise((resolve) => {
|
|
762
903
|
exitResolve = resolve;
|
|
763
904
|
});
|
|
764
|
-
const cleanupFilesystem = async () => {
|
|
765
|
-
try {
|
|
766
|
-
await rm(session.cfHomeDir, { recursive: true, force: true });
|
|
767
|
-
} catch {
|
|
768
|
-
}
|
|
769
|
-
};
|
|
770
905
|
const finalize = async () => {
|
|
771
906
|
if (!tunnelClosed) {
|
|
772
907
|
tunnelClosed = true;
|
|
@@ -778,125 +913,36 @@ async function startDebugger(options) {
|
|
|
778
913
|
}, PORT_CLEANUP_DELAY_MS);
|
|
779
914
|
}
|
|
780
915
|
await removeSession(session.sessionId);
|
|
781
|
-
await cleanupFilesystem();
|
|
916
|
+
await cleanupFilesystem(session.cfHomeDir);
|
|
782
917
|
emit("stopped");
|
|
783
918
|
};
|
|
784
919
|
try {
|
|
785
920
|
await mkdir3(session.cfHomeDir, { recursive: true });
|
|
786
|
-
emit
|
|
787
|
-
await updateSessionStatus(session.sessionId, "logging-in");
|
|
788
|
-
await cfLogin(apiEndpoint, email, password, context);
|
|
789
|
-
checkAbort(options.signal);
|
|
790
|
-
emit("targeting");
|
|
791
|
-
await updateSessionStatus(session.sessionId, "targeting");
|
|
792
|
-
await cfTarget(options.org, options.space, context);
|
|
793
|
-
checkAbort(options.signal);
|
|
921
|
+
await loginAndTarget(options, apiEndpoint, email, password, context, session.sessionId, emit);
|
|
794
922
|
await killProcessOnPort(session.localPort);
|
|
795
923
|
await new Promise((resolve) => {
|
|
796
924
|
setTimeout(resolve, 200);
|
|
797
925
|
});
|
|
798
|
-
emit
|
|
799
|
-
await
|
|
800
|
-
const signalResult = await cfSshOneShot(
|
|
801
|
-
options.app,
|
|
802
|
-
`kill -s USR1 $(pidof node)`,
|
|
803
|
-
context
|
|
804
|
-
);
|
|
805
|
-
if (isSshDisabledError(signalResult.stderr)) {
|
|
806
|
-
const alreadyEnabled = await cfSshEnabled(options.app, context);
|
|
807
|
-
if (!alreadyEnabled) {
|
|
808
|
-
emit("ssh-enabling", "Enabling SSH on the app");
|
|
809
|
-
await updateSessionStatus(session.sessionId, "ssh-enabling");
|
|
810
|
-
await cfEnableSsh(options.app, context);
|
|
811
|
-
}
|
|
812
|
-
emit("ssh-restarting", "Restarting app so SSH becomes active");
|
|
813
|
-
await updateSessionStatus(session.sessionId, "ssh-restarting");
|
|
814
|
-
await cfRestartApp(options.app, context);
|
|
815
|
-
checkAbort(options.signal);
|
|
816
|
-
emit("signaling");
|
|
817
|
-
await updateSessionStatus(session.sessionId, "signaling");
|
|
818
|
-
const retrySignalResult = await cfSshOneShot(
|
|
819
|
-
options.app,
|
|
820
|
-
`kill -s USR1 $(pidof node)`,
|
|
821
|
-
context
|
|
822
|
-
);
|
|
823
|
-
if (retrySignalResult.exitCode !== 0) {
|
|
824
|
-
const detail = retrySignalResult.stderr.trim().length > 0 ? retrySignalResult.stderr.trim() : `exit code ${String(retrySignalResult.exitCode)}`;
|
|
825
|
-
throw new CfDebuggerError(
|
|
826
|
-
"USR1_SIGNAL_FAILED",
|
|
827
|
-
`Failed to send SIGUSR1 to the Node.js process on ${options.app} after enabling SSH: ${detail}`,
|
|
828
|
-
retrySignalResult.stderr
|
|
829
|
-
);
|
|
830
|
-
}
|
|
831
|
-
} else if (signalResult.exitCode !== 0) {
|
|
832
|
-
const detail = signalResult.stderr.trim().length > 0 ? signalResult.stderr.trim() : `exit code ${String(signalResult.exitCode)}`;
|
|
833
|
-
throw new CfDebuggerError(
|
|
834
|
-
"USR1_SIGNAL_FAILED",
|
|
835
|
-
`Failed to send SIGUSR1 to the Node.js process on ${options.app}: ${detail}`,
|
|
836
|
-
signalResult.stderr
|
|
837
|
-
);
|
|
838
|
-
}
|
|
839
|
-
await new Promise((resolve) => {
|
|
840
|
-
setTimeout(resolve, POST_USR1_DELAY_MS);
|
|
841
|
-
});
|
|
842
|
-
checkAbort(options.signal);
|
|
926
|
+
await signalRemoteNode(options, context, session.sessionId, emit);
|
|
927
|
+
await waitAfterSignal(options.signal);
|
|
843
928
|
emit("tunneling");
|
|
844
929
|
await updateSessionStatus(session.sessionId, "tunneling");
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
);
|
|
930
|
+
const tunnel = await openReadyTunnel(
|
|
931
|
+
options,
|
|
932
|
+
session,
|
|
933
|
+
context,
|
|
934
|
+
tunnelReadyTimeoutMs,
|
|
935
|
+
(tunnelChild) => {
|
|
936
|
+
attachTunnelEvents(tunnelChild, () => {
|
|
937
|
+
tunnelClosed = true;
|
|
938
|
+
}, exitResolve, emit);
|
|
855
939
|
}
|
|
856
|
-
|
|
857
|
-
child =
|
|
858
|
-
if (child.pid !== void 0) {
|
|
859
|
-
await updateSessionPid(session.sessionId, child.pid);
|
|
860
|
-
}
|
|
861
|
-
child.on("close", (code) => {
|
|
862
|
-
tunnelClosed = true;
|
|
863
|
-
exitResolve?.(code);
|
|
864
|
-
});
|
|
865
|
-
child.on("error", (err) => {
|
|
866
|
-
emit("error", err.message);
|
|
867
|
-
});
|
|
868
|
-
const ready = await probeTunnelReady(session.localPort, tunnelReadyTimeoutMs);
|
|
869
|
-
checkAbort(options.signal);
|
|
870
|
-
if (!ready) {
|
|
871
|
-
throw new CfDebuggerError(
|
|
872
|
-
"TUNNEL_NOT_READY",
|
|
873
|
-
`SSH tunnel on port ${session.localPort.toString()} did not become ready within ${Math.round(tunnelReadyTimeoutMs / 1e3).toString()}s.`
|
|
874
|
-
);
|
|
875
|
-
}
|
|
876
|
-
const listeningPid = await findListeningProcessId(session.localPort);
|
|
877
|
-
const activePid = listeningPid ?? child.pid ?? session.pid;
|
|
878
|
-
if (activePid !== session.pid) {
|
|
879
|
-
await updateSessionPid(session.sessionId, activePid);
|
|
880
|
-
}
|
|
940
|
+
);
|
|
941
|
+
child = tunnel.child;
|
|
881
942
|
emit("ready");
|
|
882
943
|
const readySession = await updateSessionStatus(session.sessionId, "ready");
|
|
883
|
-
const activeSession = readySession ?? { ...session, pid: activePid, status: "ready" };
|
|
884
|
-
|
|
885
|
-
const handle = {
|
|
886
|
-
session: activeSession,
|
|
887
|
-
dispose: async () => {
|
|
888
|
-
disposePromise ??= (async () => {
|
|
889
|
-
emit("stopping");
|
|
890
|
-
await updateSessionStatus(session.sessionId, "stopping");
|
|
891
|
-
await finalize();
|
|
892
|
-
})();
|
|
893
|
-
await disposePromise;
|
|
894
|
-
},
|
|
895
|
-
waitForExit: async () => {
|
|
896
|
-
return await exitPromise;
|
|
897
|
-
}
|
|
898
|
-
};
|
|
899
|
-
return handle;
|
|
944
|
+
const activeSession = readySession ?? { ...session, pid: tunnel.activePid, status: "ready" };
|
|
945
|
+
return createHandle(activeSession, emit, finalize, exitPromise);
|
|
900
946
|
} catch (err) {
|
|
901
947
|
const message = err instanceof Error ? err.message : String(err);
|
|
902
948
|
emit("error", message);
|
|
@@ -904,6 +950,16 @@ async function startDebugger(options) {
|
|
|
904
950
|
throw err;
|
|
905
951
|
}
|
|
906
952
|
}
|
|
953
|
+
async function cleanupFilesystem(cfHomeDir) {
|
|
954
|
+
try {
|
|
955
|
+
await rm(cfHomeDir, { recursive: true, force: true });
|
|
956
|
+
} catch {
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// src/debug-session/sessions.ts
|
|
961
|
+
import { rm as rm2 } from "fs/promises";
|
|
962
|
+
import process5 from "process";
|
|
907
963
|
async function stopDebugger(options) {
|
|
908
964
|
const sessions = await pruneAndCleanupOrphans();
|
|
909
965
|
let target;
|
|
@@ -916,7 +972,7 @@ async function stopDebugger(options) {
|
|
|
916
972
|
if (target === void 0) {
|
|
917
973
|
return void 0;
|
|
918
974
|
}
|
|
919
|
-
if (target.pid !==
|
|
975
|
+
if (target.pid !== process5.pid) {
|
|
920
976
|
try {
|
|
921
977
|
await terminatePidOrGroup(target.pid);
|
|
922
978
|
} catch {
|
|
@@ -927,7 +983,7 @@ async function stopDebugger(options) {
|
|
|
927
983
|
}, PORT_CLEANUP_DELAY_MS);
|
|
928
984
|
const removed = await removeSession(target.sessionId);
|
|
929
985
|
try {
|
|
930
|
-
await
|
|
986
|
+
await rm2(target.cfHomeDir, { recursive: true, force: true });
|
|
931
987
|
} catch {
|
|
932
988
|
}
|
|
933
989
|
return removed ?? target;
|