Files
NetMesh/electron/bridges/boundedSshChannelOpen.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

354 lines
10 KiB
JavaScript

"use strict";
const { invalidateSshTransport } = require("./sshTransportInvalidation.cjs");
const DEFAULT_SSH_CHANNEL_OPEN_TIMEOUT_MS = 30_000;
const DEFAULT_SSH_CHANNEL_OPEN_RATE_LIMIT_RETRIES = 3;
const DEFAULT_SSH_CHANNEL_OPEN_RATE_LIMIT_BACKOFF_MS = 150;
function closeLateChannel(channel) {
if (!channel || typeof channel === "number") return;
try { channel.once?.("error", () => {}); } catch { /* ignore */ }
try { channel.close?.(); } catch { /* ignore */ }
try { channel.end?.(); } catch { /* ignore */ }
try { channel.destroy?.(); } catch { /* ignore */ }
}
function channelAbortError(signal, label) {
const reason = signal?.reason;
const error = reason instanceof Error ? reason : new Error(`${label} was cancelled`);
if (!error.code) error.code = "ABORT_ERR";
return error;
}
function monotonicNow() {
if (typeof performance !== "undefined" && typeof performance.now === "function") {
return performance.now();
}
return Date.now();
}
/**
* Bastion / jump hosts sometimes reject rapid session channel opens with a
* distinctive rate-limit message. The common Chinese bastion typo "offen"
* (for "often") is part of the real wire text we see in the field.
*/
function isSshChannelOpenRateLimitedError(error) {
const message = String(error?.message || error || "");
return /channelOpen\s+too\s+offen\b/i.test(message)
|| /channelOpen\s+too\s+often\b/i.test(message);
}
function sleep(ms, sleepFn = null, signal = null, label = "SSH channel retry") {
return new Promise((resolve, reject) => {
let settled = false;
let timer = null;
const cleanup = () => {
if (timer) clearTimeout(timer);
timer = null;
signal?.removeEventListener?.("abort", onAbort);
};
const finish = (error) => {
if (settled) return;
settled = true;
cleanup();
if (error) reject(error);
else resolve();
};
const onAbort = () => finish(channelAbortError(signal, label));
if (signal?.aborted) {
finish(channelAbortError(signal, label));
return;
}
signal?.addEventListener?.("abort", onAbort, { once: true });
if (typeof sleepFn === "function") {
void Promise.resolve()
.then(() => sleepFn(ms))
.then(() => finish(), (error) => finish(error));
return;
}
timer = setTimeout(() => finish(), ms);
});
}
function openBoundedSshChannel(sshClient, invoke, options = {}) {
const label = String(options.label || "SSH channel open");
const timeoutMs = Math.max(
1,
Number(options.timeoutMs) || DEFAULT_SSH_CHANNEL_OPEN_TIMEOUT_MS,
);
const signal = options.signal || null;
const closeLateResult = options.closeLateResult || closeLateChannel;
const timeoutCode = options.timeoutCode || "SSH_CHANNEL_OPEN_TIMEOUT";
const invalidateOnAbort = options.invalidateOnAbort !== false;
const invalidateOnTimeout = options.invalidateOnTimeout !== false;
const setTimeoutFn = options.setTimeoutFn || setTimeout;
const clearTimeoutFn = options.clearTimeoutFn || clearTimeout;
return new Promise((resolve, reject) => {
let settled = false;
let invoked = false;
let abandoned = false;
let timer = null;
const cleanup = () => {
if (timer) clearTimeoutFn(timer);
timer = null;
signal?.removeEventListener?.("abort", onAbort);
};
const finish = (error, result, { invalidate = false, abandon = false } = {}) => {
if (settled) {
if (result) closeLateResult(result);
if (abandoned) {
abandoned = false;
try { options.onAbandonedOpenSettled?.(); } catch { /* ignore */ }
}
return false;
}
settled = true;
cleanup();
if (error && result) closeLateResult(result);
if (abandon && invoked) {
abandoned = true;
try { options.onAbandonedOpen?.(); } catch { /* ignore */ }
}
if (invalidate) invalidateSshTransport(sshClient);
if (error) reject(error);
else resolve(result);
return true;
};
const onAbort = () => finish(
channelAbortError(signal, label),
null,
{ invalidate: invalidateOnAbort, abandon: !invalidateOnAbort },
);
if (signal?.aborted) {
finish(channelAbortError(signal, label));
return;
}
signal?.addEventListener?.("abort", onAbort, { once: true });
// This is a correctness deadline, not background housekeeping. It must
// remain referenced until the channel open settles; cleanup clears it on
// every success, error, abort, and synchronous-throw path.
timer = setTimeoutFn(() => {
const error = new Error(`${label} timed out after ${timeoutMs} ms`);
error.code = timeoutCode;
finish(error, null, {
invalidate: invalidateOnTimeout,
abandon: !invalidateOnTimeout,
});
}, timeoutMs);
try {
invoked = true;
invoke((error, result) => finish(error, result));
} catch (error) {
finish(error);
}
});
}
async function openBoundedSshShell(sshClient, windowOptions, shellOptions, options = {}) {
const hasRateLimitRetryTimeout = Number.isFinite(options.rateLimitRetryTimeoutMs);
const hasExplicitRateLimitRetries = Number.isFinite(options.rateLimitRetries);
const rateLimitRetries = Math.max(
0,
hasExplicitRateLimitRetries
? Number(options.rateLimitRetries)
: hasRateLimitRetryTimeout
? Number.POSITIVE_INFINITY
: DEFAULT_SSH_CHANNEL_OPEN_RATE_LIMIT_RETRIES,
);
const rateLimitRetryTimeoutMs = hasRateLimitRetryTimeout
? Math.max(0, Number(options.rateLimitRetryTimeoutMs))
: null;
const rateLimitBackoffMs = Math.max(
1,
Number(options.rateLimitBackoffMs) || DEFAULT_SSH_CHANNEL_OPEN_RATE_LIMIT_BACKOFF_MS,
);
const sleepFn = options.sleepFn || null;
const nowFn = typeof options.nowFn === "function" ? options.nowFn : monotonicNow;
const retryStartedAt = nowFn();
const retryDeadline = rateLimitRetryTimeoutMs === null
? null
: retryStartedAt + rateLimitRetryTimeoutMs;
let attempt = 0;
let lastRateLimitError = null;
for (;;) {
const attemptStartedAt = nowFn();
if (
lastRateLimitError
&& retryDeadline !== null
&& attemptStartedAt >= retryDeadline
) {
throw lastRateLimitError;
}
const configuredAttemptTimeoutMs = Math.max(
1,
Number(options.timeoutMs) || DEFAULT_SSH_CHANNEL_OPEN_TIMEOUT_MS,
);
const isRateLimitRetry = lastRateLimitError !== null;
const attemptTimeoutMs = retryDeadline === null || !isRateLimitRetry
? configuredAttemptTimeoutMs
: Math.max(
1,
Math.min(configuredAttemptTimeoutMs, Math.ceil(retryDeadline - attemptStartedAt)),
);
const retryBudgetConstrainsAttempt = isRateLimitRetry
&& attemptTimeoutMs < configuredAttemptTimeoutMs;
try {
return await openBoundedSshChannel(
sshClient,
(callback) => sshClient.shell(windowOptions, shellOptions, callback),
{
...options,
timeoutMs: attemptTimeoutMs,
invalidateOnTimeout: retryBudgetConstrainsAttempt
? false
: options.invalidateOnTimeout,
label: options.label || "SSH shell channel open",
timeoutCode: "SSH_SHELL_OPEN_TIMEOUT",
},
);
} catch (error) {
if (
retryBudgetConstrainsAttempt
&& error?.code === "SSH_SHELL_OPEN_TIMEOUT"
&& lastRateLimitError
) {
throw lastRateLimitError;
}
if (!isSshChannelOpenRateLimitedError(error) || options.signal?.aborted) {
throw error;
}
lastRateLimitError = error;
const nextDelayMs = rateLimitBackoffMs * (attempt + 1);
const retryTimeoutExpired = rateLimitRetryTimeoutMs !== null
&& nowFn() + nextDelayMs >= retryDeadline;
if (
attempt >= rateLimitRetries
|| retryTimeoutExpired
) {
throw error;
}
attempt += 1;
await sleep(
nextDelayMs,
sleepFn,
options.signal,
options.label || "SSH shell channel retry",
);
}
}
}
function openBoundedForwardOut(
sshClient,
sourceAddress,
sourcePort,
targetAddress,
targetPort,
options = {},
) {
return openBoundedSshChannel(
sshClient,
(callback) => sshClient.forwardOut(
sourceAddress,
sourcePort,
targetAddress,
targetPort,
callback,
),
{
...options,
label: options.label || "SSH forwardOut channel open",
timeoutCode: "SSH_FORWARD_OUT_TIMEOUT",
},
);
}
function openBoundedForwardIn(sshClient, bindAddress, bindPort, options = {}) {
return openBoundedSshChannel(
sshClient,
(callback) => sshClient.forwardIn(bindAddress, bindPort, callback),
{
...options,
label: options.label || "SSH forwardIn request",
timeoutCode: "SSH_FORWARD_IN_TIMEOUT",
closeLateResult: () => {},
},
);
}
function deliverChannelOpenCallback(promise, callback) {
void promise.then(
(result) => callback(null, result),
(error) => callback(error),
);
}
function openBoundedSshShellCallback(
sshClient,
windowOptions,
shellOptions,
callback,
options = {},
) {
deliverChannelOpenCallback(
openBoundedSshShell(sshClient, windowOptions, shellOptions, options),
callback,
);
}
function openBoundedForwardOutCallback(
sshClient,
sourceAddress,
sourcePort,
targetAddress,
targetPort,
callback,
options = {},
) {
deliverChannelOpenCallback(
openBoundedForwardOut(
sshClient,
sourceAddress,
sourcePort,
targetAddress,
targetPort,
options,
),
callback,
);
}
function openBoundedForwardInCallback(
sshClient,
bindAddress,
bindPort,
callback,
options = {},
) {
deliverChannelOpenCallback(
openBoundedForwardIn(sshClient, bindAddress, bindPort, options),
callback,
);
}
module.exports = {
DEFAULT_SSH_CHANNEL_OPEN_TIMEOUT_MS,
DEFAULT_SSH_CHANNEL_OPEN_RATE_LIMIT_RETRIES,
DEFAULT_SSH_CHANNEL_OPEN_RATE_LIMIT_BACKOFF_MS,
closeLateChannel,
isSshChannelOpenRateLimitedError,
openBoundedSshChannel,
openBoundedSshShell,
openBoundedSshShellCallback,
openBoundedForwardOut,
openBoundedForwardOutCallback,
openBoundedForwardIn,
openBoundedForwardInCallback,
};