[Init] Initial commit - NetMesh terminal manager
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
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
This commit is contained in:
353
electron/bridges/boundedSshChannelOpen.cjs
Normal file
353
electron/bridges/boundedSshChannelOpen.cjs
Normal file
@@ -0,0 +1,353 @@
|
||||
"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,
|
||||
};
|
||||
Reference in New Issue
Block a user