mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-03 20:42:33 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8740a94b44 | ||
|
|
bef677d9c5 |
No files matched your search
@@ -564,6 +564,11 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
const reconnectWakeInvalidateModeRef = useRef<"dispose" | "keep">("dispose");
|
||||
const reconnectWakeTokenRef = useRef<symbol | null>(null);
|
||||
const manualReconnectRequestRef = useRef<() => void>(() => {});
|
||||
// Once a pane reconnects, every later SSH attempt in it must dial a fresh
|
||||
// connection: reusing a live/idle pooled transport multiplexes onto the old
|
||||
// login, so remote supplementary-group changes (e.g. `usermod -aG`) are not
|
||||
// picked up until the app fully quits (#3293). See createTerminalSessionStarters.
|
||||
const requireFreshConnectionOnReconnectRef = useRef(false);
|
||||
const terminalDataCapturedRef = useRef(false);
|
||||
const connectionLogBufferRef = useRef(createConnectionLogBuffer(MAX_CONNECTION_LOG_DATA_CHARS));
|
||||
const terminalOutputHistoryRef = useRef(createTerminalOutputHistoryPreview());
|
||||
@@ -2375,6 +2380,7 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
sessionId,
|
||||
reuseConnectionFromSessionIdRef: reuseConnectionSourceRef,
|
||||
requireFreshConnection,
|
||||
requireFreshConnectionOnReconnectRef,
|
||||
reuseConnectionSourceAttemptedRef,
|
||||
setConnectionReuseAttemptSourceId,
|
||||
shouldUseFreshSshConnection: () => {
|
||||
@@ -3627,6 +3633,10 @@ const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
const retryStillActive = () => retryTokenStillCurrent() && termRef.current === term;
|
||||
|
||||
bootEpochRef.current += 1;
|
||||
// This start is a reconnect: force the bridge to dial a fresh connection
|
||||
// instead of borrowing a parked/live transport, so the server performs a
|
||||
// new login and picks up remote supplementary-group changes (#3293).
|
||||
requireFreshConnectionOnReconnectRef.current = true;
|
||||
publishBootEpoch();
|
||||
isBootActiveRef.current = true;
|
||||
auth.resetForRetry();
|
||||
|
||||
@@ -481,9 +481,9 @@ test("startSSH requests a fresh transport for ordinary opens with connection aut
|
||||
assert.equal(captured[0].sourceSessionId, "source-session");
|
||||
assert.equal(captured[1].reuseTransport, false);
|
||||
assert.equal(captured[1].sourceSessionId, undefined);
|
||||
assert.equal(captured[2].reuseTransport, undefined);
|
||||
assert.equal(captured[2].reuseTransport, false);
|
||||
assert.equal(captured[2].sourceSessionId, undefined);
|
||||
assert.equal(captured[3].reuseTransport, undefined);
|
||||
assert.equal(captured[3].reuseTransport, false);
|
||||
assert.deepEqual(reuseAttempts, ["source-session", undefined, undefined]);
|
||||
});
|
||||
|
||||
@@ -502,9 +502,7 @@ test("startSSH requests a fresh transport for Duplicate Session clones", async (
|
||||
resizeSession: noop,
|
||||
};
|
||||
|
||||
// Without the marker an ordinary open stays eligible for general endpoint
|
||||
// reuse; with it, every attempt (including reconnects) dials fresh instead
|
||||
// of borrowing the source's live authenticated transport.
|
||||
// Ordinary opens and Duplicate Session both require a new login.
|
||||
await createTerminalSessionStarters(createStarterContext({
|
||||
shouldUseFreshSshConnection: () => false,
|
||||
terminalBackend,
|
||||
@@ -522,12 +520,65 @@ test("startSSH requests a fresh transport for Duplicate Session clones", async (
|
||||
await duplicateStarters.startSSH(createTermStub() as never);
|
||||
await duplicateStarters.startSSH(createTermStub() as never);
|
||||
|
||||
assert.equal(captured[0].reuseTransport, undefined);
|
||||
assert.equal(captured[0].reuseTransport, false);
|
||||
assert.equal(captured[0].sourceSessionId, undefined);
|
||||
assert.equal(captured[1].reuseTransport, false);
|
||||
assert.equal(captured[2].reuseTransport, false);
|
||||
});
|
||||
|
||||
test("startSSH dials a fresh transport once a pane has reconnected (#3293)", async () => {
|
||||
const captured: Record<string, unknown>[] = [];
|
||||
const terminalBackend = {
|
||||
backendAvailable: () => true,
|
||||
startSSHSession: async (options: Record<string, unknown>) => {
|
||||
captured.push(options);
|
||||
return `ssh-session-${captured.length}`;
|
||||
},
|
||||
onSessionData: () => noop,
|
||||
onSessionExit: () => noop,
|
||||
onChainProgress: () => noop,
|
||||
writeToSession: noop,
|
||||
resizeSession: noop,
|
||||
};
|
||||
|
||||
// An ordinary new tab must refresh groups even before its first reconnect.
|
||||
await createTerminalSessionStarters(createStarterContext({
|
||||
shouldUseFreshSshConnection: () => false,
|
||||
requireFreshConnectionOnReconnectRef: { current: false },
|
||||
terminalBackend,
|
||||
}) as never).startSSH(createTermStub() as never);
|
||||
assert.equal(captured[0].reuseTransport, false);
|
||||
|
||||
// After a reconnect (manual retry / auto-reconnect) every attempt must dial
|
||||
// a brand-new connection so the server performs a fresh login and picks up
|
||||
// remote supplementary-group changes (e.g. `usermod -aG`).
|
||||
const reconnectedStarters = createTerminalSessionStarters(createStarterContext({
|
||||
shouldUseFreshSshConnection: () => false,
|
||||
requireFreshConnectionOnReconnectRef: { current: true },
|
||||
terminalBackend,
|
||||
}) as never);
|
||||
await reconnectedStarters.startSSH(createTermStub() as never);
|
||||
await reconnectedStarters.startSSH(createTermStub() as never);
|
||||
assert.equal(captured[1].reuseTransport, false);
|
||||
assert.equal(captured[2].reuseTransport, false);
|
||||
|
||||
// Copy/Split may still have an unconsumed source while credentials load.
|
||||
// A reconnect must discard that intent before the next backend attempt.
|
||||
const pendingSource = { current: "source-session" as string | undefined };
|
||||
const reuseAttempts: (string | undefined)[] = [];
|
||||
await createTerminalSessionStarters(createStarterContext({
|
||||
shouldUseFreshSshConnection: () => false,
|
||||
requireFreshConnectionOnReconnectRef: { current: true },
|
||||
reuseConnectionFromSessionIdRef: pendingSource,
|
||||
setConnectionReuseAttemptSourceId: (id: string | undefined) => reuseAttempts.push(id),
|
||||
terminalBackend,
|
||||
}) as never).startSSH(createTermStub() as never);
|
||||
assert.equal(captured[3].reuseTransport, false);
|
||||
assert.equal(captured[3].sourceSessionId, undefined);
|
||||
assert.equal(pendingSource.current, undefined);
|
||||
assert.deepEqual(reuseAttempts, [undefined]);
|
||||
});
|
||||
|
||||
test("startSSH commits an empty automation snapshot only after the backend session succeeds", async () => {
|
||||
let resolveStart: ((sessionId: string) => void) | undefined;
|
||||
let commitCount = 0;
|
||||
@@ -605,7 +656,7 @@ test("startSSH rechecks the live automation policy before password fallback", as
|
||||
await createTerminalSessionStarters(ctx as never).startSSH(createTermStub() as never);
|
||||
|
||||
assert.equal(captured.length, 2);
|
||||
assert.equal(captured[0].reuseTransport, undefined);
|
||||
assert.equal(captured[0].reuseTransport, false);
|
||||
assert.equal(captured[1].reuseTransport, false);
|
||||
assert.equal(commitCount, 0);
|
||||
});
|
||||
|
||||
@@ -616,22 +616,20 @@ export const createTerminalSessionStarters = (ctx: TerminalSessionStartersContex
|
||||
? ctx.host.identityFilePaths
|
||||
: undefined;
|
||||
|
||||
let sourceReuseAttemptedWithinStart = false;
|
||||
const startAttempt = async (attempt: {
|
||||
password?: string;
|
||||
key?: SSHKey;
|
||||
useIdentityFiles?: boolean;
|
||||
useSshAgent?: boolean;
|
||||
}): Promise<string> => {
|
||||
const sourceSessionId = ctx.reuseConnectionFromSessionIdRef?.current;
|
||||
const sourceReuseAttempted = ctx.reuseConnectionSourceAttemptedRef?.current
|
||||
?? sourceReuseAttemptedWithinStart;
|
||||
const isFallbackAfterSourceReuse = sourceReuseAttempted && !sourceSessionId;
|
||||
// Reconnect supersedes a Copy/Split intent that was still waiting for credentials.
|
||||
const sourceSessionId = ctx.requireFreshConnectionOnReconnectRef?.current
|
||||
? undefined
|
||||
: ctx.reuseConnectionFromSessionIdRef?.current;
|
||||
if (ctx.reuseConnectionFromSessionIdRef) {
|
||||
ctx.reuseConnectionFromSessionIdRef.current = undefined;
|
||||
}
|
||||
if (sourceSessionId) {
|
||||
sourceReuseAttemptedWithinStart = true;
|
||||
if (ctx.reuseConnectionSourceAttemptedRef) {
|
||||
ctx.reuseConnectionSourceAttemptedRef.current = true;
|
||||
}
|
||||
@@ -704,21 +702,14 @@ export const createTerminalSessionStarters = (ctx: TerminalSessionStartersContex
|
||||
// bridge silently falls back to a fresh connection if the source is
|
||||
// gone, so reconnect/retry after the source closed still works.
|
||||
sourceSessionId,
|
||||
// Connect-time automation must see the complete login sequence. An
|
||||
// explicit Copy/Split keeps its source-session reuse contract, while
|
||||
// an ordinary open bypasses endpoint/idle transport reuse. Duplicate
|
||||
// Session clones must never borrow the source's live transport
|
||||
// either, so they send an explicit `reuseTransport: false`.
|
||||
reuseTransport: !sourceSessionId
|
||||
&& (requiresFreshSshConnection || isFallbackAfterSourceReuse || ctx.requireFreshConnection === true)
|
||||
? false
|
||||
: undefined,
|
||||
// Only an explicit Copy/Split may share an existing login. Ordinary
|
||||
// opens and reconnects must authenticate again to refresh remote groups.
|
||||
reuseTransport: sourceSessionId ? undefined : false,
|
||||
skipShellPidDiscovery: ctx.isNetworkDevice === true,
|
||||
});
|
||||
if (!requiresFreshSshConnection) {
|
||||
ctx.onConnectAutomationSnapshotCommitted?.();
|
||||
}
|
||||
sourceReuseAttemptedWithinStart = false;
|
||||
if (ctx.reuseConnectionSourceAttemptedRef) {
|
||||
ctx.reuseConnectionSourceAttemptedRef.current = false;
|
||||
}
|
||||
|
||||
@@ -154,6 +154,13 @@ export type TerminalSessionStartersContext = {
|
||||
// SSH attempt must send `reuseTransport: false` so the bridge never borrows
|
||||
// the source's live or any other pooled transport.
|
||||
requireFreshConnection?: boolean;
|
||||
// Set by the reconnect path (manual retry / auto-reconnect) for the rest of
|
||||
// the pane's lifetime: every SSH attempt must dial a brand-new connection
|
||||
// instead of borrowing a live or idle pooled transport. Reusing an
|
||||
// already-authenticated connection skips the server-side login, so remote
|
||||
// supplementary-group changes (e.g. `usermod -aG`) stay invisible until the
|
||||
// whole app quits (#3293).
|
||||
requireFreshConnectionOnReconnectRef?: MutableRefObject<boolean>;
|
||||
// Persists across renderer auth retries after the one-shot source intent is
|
||||
// consumed. Cleared only after a backend session starts successfully.
|
||||
reuseConnectionSourceAttemptedRef?: MutableRefObject<boolean>;
|
||||
|
||||
Reference in new issue
Block a user