Files
NetMesh/electron/bridges/sshIdleParkPolicy.cjs
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

132 lines
4.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
/**
* Policy for parking an authenticated SSH transport after the last shell
* closes (ControlPersist-style reuse).
*
* Some bastions bind the TCP/SSH connection to the first interactive session.
* After that session ends they still accept a new session channel, then
* immediately EOF / exit 0. Netcatty used to treat that as a clean user exit
* and close the tab (issue #2923, 齐治 TERM-SSHD).
*/
/** How long an idle-park reconnect may wait for a reused shell to stay open. */
const DEFAULT_REUSED_SHELL_LIVENESS_MS = 350;
function normalizeRemoteSshVersion(remoteSshVersion) {
return String(remoteSshVersion || "").trim().replace(/^SSH-(?:2\.0|1\.99)-/i, "");
}
/**
* Whether this daemon can host a new interactive shell on a parked transport.
* Unknown banners return true; those go through a post-open liveness check.
*/
function remoteAllowsIdleParkedShellReuse(remoteSshVersion) {
const software = normalizeRemoteSshVersion(remoteSshVersion);
// 齐治 / QiZhi TERM-SSHD: second shell on a parked conn exits in ~130170ms.
if (/TERM-SSHD/i.test(software)) return false;
return true;
}
/**
* Idle-park reconnects to unknown banners wait briefly to see if the new
* shell dies immediately. OpenSSH / Dropbear multiplex cleanly and skip it.
*/
function remoteNeedsReusedShellLivenessCheck(remoteSshVersion) {
if (!remoteAllowsIdleParkedShellReuse(remoteSshVersion)) return true;
const software = normalizeRemoteSshVersion(remoteSshVersion);
if (!software) return true;
if (/^OpenSSH[_-]/i.test(software)) return false;
if (/^dropbear/i.test(software)) return false;
return true;
}
/**
* Idle park is not the only "last shell already left" state. An SFTP or
* forward lease can keep the transport `live` after the interactive shell
* returns; `pendingShellReconnectRisk` is recorded in that case. Those
* reconnects need the same settle check as a parked transport.
*/
function shouldConfirmReusedShellLiveness({
state,
pendingShellReconnectRisk,
remoteSshVersion,
} = {}) {
if (!remoteNeedsReusedShellLivenessCheck(remoteSshVersion)) return false;
return state === "idle" || Boolean(pendingShellReconnectRisk);
}
function resolveReusedShellLivenessMs(value) {
const n = Number(value);
if (!Number.isFinite(n) || n < 0) return DEFAULT_REUSED_SHELL_LIVENESS_MS;
return Math.min(5_000, Math.round(n));
}
/**
* Watch a just-opened reused shell. Resolves `{ alive: false }` if the channel
* exits/closes before `settleMs`. Buffers stdout so the caller can replay it
* after wiring the real session handlers.
*/
function waitForReusedShellLiveness(stream, opts = {}) {
const settleMs = resolveReusedShellLivenessMs(
opts.settleMs === undefined ? DEFAULT_REUSED_SHELL_LIVENESS_MS : opts.settleMs,
);
const schedule = typeof opts.setTimeout === "function" ? opts.setTimeout : setTimeout;
const cancel = typeof opts.clearTimeout === "function" ? opts.clearTimeout : clearTimeout;
return new Promise((resolve) => {
if (!stream || stream.destroyed || stream.closed) {
resolve({ alive: false, reason: "already-closed", buffered: [] });
return;
}
let settled = false;
const buffered = [];
const finish = (result) => {
if (settled) return;
settled = true;
cleanup();
resolve({ ...result, buffered });
};
const onData = (chunk) => {
buffered.push(chunk);
};
const onExit = (code, signal) => {
finish({ alive: false, reason: "exit", code, signal });
};
const onClose = () => {
finish({ alive: false, reason: "close" });
};
const onError = (error) => {
finish({ alive: false, reason: "error", error });
};
stream.on("data", onData);
stream.on("exit", onExit);
stream.on("close", onClose);
stream.on("error", onError);
const timer = schedule(() => {
finish({ alive: true, reason: "settle" });
}, settleMs);
function cleanup() {
cancel(timer);
stream.removeListener("data", onData);
stream.removeListener("exit", onExit);
stream.removeListener("close", onClose);
stream.removeListener("error", onError);
}
});
}
module.exports = {
DEFAULT_REUSED_SHELL_LIVENESS_MS,
remoteAllowsIdleParkedShellReuse,
remoteNeedsReusedShellLivenessCheck,
shouldConfirmReusedShellLiveness,
resolveReusedShellLivenessMs,
waitForReusedShellLiveness,
};