fix: keep active ssh downloads alive

This commit is contained in:
Codex
2026-06-11 18:08:56 +00:00
parent 0a9d07c760
commit a8d49845ca
5 changed files with 132 additions and 23 deletions
+56 -11
View File
@@ -2499,12 +2499,23 @@ const openTimer = setTimeout(() => {
process.exit(255);
}, Number(open.openTimeoutMs || 15000));
const runtimeTimeoutMs = Number(open.runtimeTimeoutMs || 60000);
const runtimeTimer = setTimeout(() => {
process.stderr.write("unidesk ssh bridge runtime timeout; use short query plus poll semantics instead of keeping tran open\n");
exitCode = 124;
try { ws.close(); } catch {}
setTimeout(() => process.exit(124), 250).unref?.();
}, runtimeTimeoutMs);
const runtimeTimeoutMode = open.runtimeTimeoutMode === "inactivity" ? "inactivity" : "wall-clock";
let runtimeTimer = null;
function armRuntimeTimer() {
if (runtimeTimer !== null) clearTimeout(runtimeTimer);
runtimeTimer = setTimeout(() => {
const noun = runtimeTimeoutMode === "inactivity" ? "inactivity timeout" : "runtime timeout";
process.stderr.write("unidesk ssh bridge " + noun + "; use short query plus poll semantics for long non-streaming work\n");
exitCode = 124;
try { ws.close(); } catch {}
setTimeout(() => process.exit(124), 250).unref?.();
}, runtimeTimeoutMs);
}
function clearRuntimeTimer() {
if (runtimeTimer !== null) clearTimeout(runtimeTimer);
runtimeTimer = null;
}
armRuntimeTimer();
function send(value) {
const text = JSON.stringify(value);
@@ -2557,6 +2568,7 @@ ws.addEventListener("open", () => {
ws.addEventListener("message", (event) => {
const message = JSON.parse(decodeData(event.data));
if (runtimeTimeoutMode === "inactivity") armRuntimeTimer();
if (message.type === "ssh.data") {
opened = true;
const chunk = Buffer.from(message.data || "", "base64");
@@ -2577,7 +2589,7 @@ ws.addEventListener("message", (event) => {
}
if (message.type === "ssh.error") {
clearTimeout(openTimer);
clearTimeout(runtimeTimer);
clearRuntimeTimer();
process.stderr.write(String(message.message || "ssh bridge error") + "\n");
exitCode = 255;
ws.close();
@@ -2585,7 +2597,7 @@ ws.addEventListener("message", (event) => {
}
if (message.type === "ssh.exit") {
clearTimeout(openTimer);
clearTimeout(runtimeTimer);
clearRuntimeTimer();
exitCode = Number.isInteger(message.exitCode) ? message.exitCode : 255;
ws.close();
}
@@ -2593,7 +2605,7 @@ ws.addEventListener("message", (event) => {
ws.addEventListener("close", () => {
clearTimeout(openTimer);
clearTimeout(runtimeTimer);
clearRuntimeTimer();
process.exit(exitCode);
});
@@ -2854,6 +2866,7 @@ async function runSshStreamRemoteCommand(
remoteCommand: string,
handlers: SshRemoteCommandStreamHandlers,
input?: string,
options: { inactivityTimeoutMs?: number } = {},
): Promise<SshCaptureResult> {
const streamInvocation = {
...invocation,
@@ -2861,7 +2874,8 @@ async function runSshStreamRemoteCommand(
};
const startedAtMs = Date.now();
const size = terminalSize();
const runtimeTimeoutMs = sshRuntimeTimeoutMs();
const inactivityTimeoutMs = options.inactivityTimeoutMs ?? sshRuntimeTimeoutMs();
const runtimeTimeoutMs = options.inactivityTimeoutMs === undefined ? inactivityTimeoutMs : Math.max(inactivityTimeoutMs * 4, 60 * 60_000);
const payload = {
providerId: invocation.providerId,
command: wrapSshRemoteCommand(remoteCommand),
@@ -2870,6 +2884,7 @@ async function runSshStreamRemoteCommand(
stdinEotOnEnd: false,
openTimeoutMs: Math.max(15000, Number(process.env.UNIDESK_SSH_OPEN_TIMEOUT_MS || 60000)),
runtimeTimeoutMs,
runtimeTimeoutMode: options.inactivityTimeoutMs === undefined ? "wall-clock" : "inactivity",
cols: size.cols,
rows: size.rows,
};
@@ -2924,10 +2939,37 @@ async function runSshStreamRemoteCommand(
return await new Promise<SshCaptureResult>((resolve) => {
let settled = false;
let killTimer: NodeJS.Timeout | null = null;
let inactivityTimer: NodeJS.Timeout | null = null;
const refreshActivityTimer = (): void => {
if (settled || options.inactivityTimeoutMs === undefined) return;
if (inactivityTimer !== null) clearTimeout(inactivityTimer);
inactivityTimer = setTimeout(() => {
const hint = formatSshRuntimeTimeoutHint(sshRuntimeTimeoutHint({
invocation: streamInvocation,
transport: "backend-core-broker",
timeoutMs: inactivityTimeoutMs,
}));
stderr += hint;
try {
child.kill("SIGTERM");
} catch {
// Ignore.
}
killTimer = setTimeout(() => {
try {
child.kill("SIGKILL");
} catch {
// Ignore.
}
}, 2000);
finish(124);
}, inactivityTimeoutMs);
};
const finish = (exitCode: number): void => {
if (settled) return;
settled = true;
clearTimeout(runtimeTimer);
if (inactivityTimer !== null) clearTimeout(inactivityTimer);
if (killTimer !== null) clearTimeout(killTimer);
void streamWrites.then(() => {
const finalCode = streamError === null ? exitCode : 255;
@@ -2962,11 +3004,14 @@ async function runSshStreamRemoteCommand(
}, 2000);
finish(124);
}, runtimeTimeoutMs);
refreshActivityTimer();
child.on("error", (error) => {
stderr += `unidesk ssh failed to start broker: ${error.message}\n`;
finish(255);
});
child.on("close", (code) => finish(code ?? 255));
child.stdout.on("data", refreshActivityTimer);
child.stderr.on("data", refreshActivityTimer);
});
}
@@ -3077,7 +3122,7 @@ export async function runSsh(config: UniDeskConfig, providerId: string, args: st
if (isSshFileTransferOperation(normalizedArgs)) {
const executor: SshRemoteCommandExecutor = {
runRemoteCommand: (remoteCommand, input) => runSshCaptureRemoteCommand(config, invocation, remoteCommand, input),
streamRemoteCommand: (remoteCommand, handlers, input) => runSshStreamRemoteCommand(config, invocation, remoteCommand, handlers, input),
streamRemoteCommand: (remoteCommand, handlers, input, options) => runSshStreamRemoteCommand(config, invocation, remoteCommand, handlers, input, options),
};
return await runSshFileTransferOperation(invocation, normalizedArgs, executor, {
buildRouteCommand: remoteCommandForRoute,