fix: keep active ssh downloads alive
This commit is contained in:
+56
-11
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user