Compare commits

...
4 changed files with 64 additions and 6 deletions

No files matched your search

@@ -179,3 +179,25 @@ test("exit messages without a reported code stay plain instead of showing a sent
);
assert.equal(formatSessionClosedExitMessage({}), "\r\n[session closed]");
});
test("infrastructure teardowns must not render a numeric code as a clean exit (#3631)", () => {
// A terminal-worker exit broadcast carries the worker's own exit status
// (graceful drain = 0). It belongs to the infrastructure failure, not to
// any remote shell, so it must never read as "session closed (code 0)".
assert.equal(
formatSessionClosedExitMessage({
reason: "error",
exitCode: 0,
error: "Terminal worker exited with code 0",
}),
"\r\n[session closed unexpectedly]",
);
assert.equal(
formatSessionClosedExitMessage({ reason: "error", exitCode: 1 }),
"\r\n[session closed unexpectedly]",
);
assert.equal(
formatSessionClosedExitMessage({ reason: "error" }),
"\r\n[session closed unexpectedly]",
);
});
@@ -22,6 +22,10 @@ export function formatSessionClosedExitMessage(evt: TerminalSessionExitEvent): s
if (evt?.reason === "signaled" || (typeof evt?.exitSignal === "string" && evt.exitSignal.trim().length > 0)) {
return "\r\n[session closed by remote signal]";
}
// Infrastructure exit codes do not describe the remote shell's status.
if (evt?.reason === "error") {
return "\r\n[session closed unexpectedly]";
}
return `\r\n[session closed${evt?.exitCode !== undefined ? ` (code ${evt.exitCode})` : ""}]`;
}
@@ -2474,6 +2474,31 @@ test("a connection close without an exit-signal stays a transport close", async
const exits = getExitPayloads(sender);
assert.equal(exits.length, 1);
assert.equal(exits[0].reason, "closed");
assert.equal(exits[0].exitCode, 0);
assert.equal(exits[0].exitCode, undefined);
assert.equal(exits[0].remoteClosedWithoutExitStatus, true);
assert.equal(exits[0].exitSignal, undefined);
});
for (const exitCode of [0, 7]) {
test(`connection close preserves reported shell status ${exitCode} before channel finalization`, async (t) => {
const { bridge } = loadBridgeWithMockedSsh2(t, { connectReady: true });
const sessions = new Map();
const start = registerStartHandler(bridge, sessions);
const sender = makeSender();
const sessionId = `status-then-conn-close-${exitCode}`;
await start({ sender }, {
hostname: "10.0.0.50", username: "alice", port: 22,
authMethod: "password", password: "secret",
useSshAgent: false, verifyHostKeys: false, sessionId,
});
const session = sessions.get(sessionId);
session.stream.emit("exit", exitCode);
session.conn.emit("close");
await waitForExitDeliveries();
const exits = getExitPayloads(sender);
assert.equal(exits.length, 1);
assert.equal(exits[0].reason, "exited");
assert.equal(exits[0].exitCode, exitCode);
assert.equal(exits[0].remoteClosedWithoutExitStatus, undefined);
});
}
+12 -5
View File
@@ -698,6 +698,9 @@ function createStartSessionApi(ctx) {
if (streamExitSignal && sessions.get(sessionId) === session) {
session._remoteExitSignal = streamExitSignal;
}
if (streamExited && sessions.get(sessionId) === session) {
session._remoteExitCode = streamExitCode;
}
});
let closeFinalized = false;
@@ -2664,12 +2667,16 @@ function createStartSessionApi(ctx) {
idleTimedOut: false,
}));
} else {
safeSendSessionExit({ safeSend, electronModule, sessions }, contents, sessionId, {
// Preserve a shell status already received before transport
// close; otherwise do not invent a successful exit code.
const shellExited = typeof session?._remoteExitCode === "number";
safeSendSessionExit({ safeSend, electronModule, sessions }, contents, sessionId, buildRemoteShellExitPayload({
sessionId,
exitCode: 0,
reason: "closed",
_terminalSessionGeneration: session?._terminalSessionGeneration,
});
generation: session?._terminalSessionGeneration,
exitCode: session?._remoteExitCode,
shellExited,
idleTimedOut: shellExited && looksLikeIdleAutoLogout(session?._promptTrackTail),
}));
}
// Use this connection's captured token so a late close from an
// old transport can't stop a newer same-sessionId stream (#916).