fix: cap tran runtime and remove local lock

This commit is contained in:
Codex
2026-05-25 23:48:03 +00:00
parent 15f5a49375
commit 8af5aafb9e
6 changed files with 200 additions and 124 deletions
+96 -1
View File
@@ -57,12 +57,28 @@ export interface SshRuntimeTimingHint {
note: string;
}
export interface SshRuntimeTimeoutHint {
code: "ssh-runtime-timeout";
level: "warning";
providerId: string;
route: string;
transport: "backend-core-broker" | "frontend-websocket";
invocationKind: SshInvocationKind;
timeoutMs: number;
timeoutSeconds: number;
message: string;
action: string;
note: string;
}
const argvQuotedSshSubcommands = new Set(["git", "rg", "grep", "sed", "nl", "stat", "du", "ls", "cat", "head", "tail", "wc", "pwd"]);
const nativeK3sKubeconfig = "/etc/rancher/k3s/k3s.yaml";
const windowsBridgeCwd = "/mnt/c/Windows";
const windowsPowerShellExePath = "/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe";
const windowsCmdExeNativePath = "C:\\Windows\\System32\\cmd.exe";
const defaultSshSlowWarningMs = 10_000;
const defaultSshRuntimeTimeoutMs = 60_000;
const maxSshRuntimeTimeoutMs = 60_000;
const k3sResourceKindAliases = new Set(["pod", "po", "pods", "deployment", "deploy", "deployments", "statefulset", "sts", "daemonset", "ds", "job", "jobs"]);
const legacyK3sOperationRouteSegments = new Set([
"guard",
@@ -1842,6 +1858,38 @@ export function formatSshRuntimeTimingHint(hint: SshRuntimeTimingHint): string {
return `UNIDESK_SSH_TIMING ${JSON.stringify(hint)}\n`;
}
export function sshRuntimeTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
const raw = env.UNIDESK_SSH_RUNTIME_TIMEOUT_MS ?? env.UNIDESK_TRAN_RUNTIME_TIMEOUT_MS;
const parsed = raw === undefined ? NaN : Number(raw);
if (!Number.isFinite(parsed) || parsed <= 0) return defaultSshRuntimeTimeoutMs;
return Math.min(maxSshRuntimeTimeoutMs, Math.max(1000, Math.trunc(parsed)));
}
export function sshRuntimeTimeoutHint(options: {
invocation: ParsedSshInvocation;
transport: SshRuntimeTimeoutHint["transport"];
timeoutMs: number;
}): SshRuntimeTimeoutHint {
const timeoutSeconds = Number((options.timeoutMs / 1000).toFixed(3));
return {
code: "ssh-runtime-timeout",
level: "warning",
providerId: safeProviderId(options.invocation.providerId),
route: options.invocation.route.raw,
transport: options.transport,
invocationKind: options.invocation.parsed.invocationKind,
timeoutMs: options.timeoutMs,
timeoutSeconds,
message: `ssh/tran operation exceeded the ${timeoutSeconds}s top-level runtime limit and was disconnected.`,
action: "Use short query plus poll semantics; do not keep tran open waiting for long CI/CD, trace, logs, or build progress.",
note: "Timeout hint is written to stderr and intentionally does not echo the original remote command.",
};
}
export function formatSshRuntimeTimeoutHint(hint: SshRuntimeTimeoutHint): string {
return `UNIDESK_SSH_RUNTIME_TIMEOUT ${JSON.stringify(hint)}\n`;
}
function brokerSource(): string {
return String.raw`
const open = JSON.parse(process.argv[2] || process.argv[1] || "{}");
@@ -1861,6 +1909,13 @@ const openTimer = setTimeout(() => {
try { ws.close(); } catch {}
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);
function send(value) {
const text = JSON.stringify(value);
@@ -1933,6 +1988,7 @@ ws.addEventListener("message", (event) => {
}
if (message.type === "ssh.error") {
clearTimeout(openTimer);
clearTimeout(runtimeTimer);
process.stderr.write(String(message.message || "ssh bridge error") + "\n");
exitCode = 255;
ws.close();
@@ -1940,12 +1996,15 @@ ws.addEventListener("message", (event) => {
}
if (message.type === "ssh.exit") {
clearTimeout(openTimer);
clearTimeout(runtimeTimer);
exitCode = Number.isInteger(message.exitCode) ? message.exitCode : 255;
ws.close();
}
});
ws.addEventListener("close", () => {
clearTimeout(openTimer);
clearTimeout(runtimeTimer);
process.exit(exitCode);
});
@@ -1979,6 +2038,7 @@ export async function runSsh(config: UniDeskConfig, providerId: string, args: st
const startedAtMs = Date.now();
const size = terminalSize();
const openTimeoutMs = Math.max(15000, Number(process.env.UNIDESK_SSH_OPEN_TIMEOUT_MS || 60000));
const runtimeTimeoutMs = sshRuntimeTimeoutMs();
const payload = {
providerId: invocation.providerId,
command: wrapSshRemoteCommand(parsed.remoteCommand, parsed.requiredHelpers),
@@ -1986,6 +2046,7 @@ export async function runSsh(config: UniDeskConfig, providerId: string, args: st
tty: parsed.remoteCommand === null,
stdinEotOnEnd: parsed.remoteCommand !== null,
openTimeoutMs,
runtimeTimeoutMs,
cols: size.cols,
rows: size.rows,
};
@@ -2039,7 +2100,41 @@ export async function runSsh(config: UniDeskConfig, providerId: string, args: st
return await new Promise<number>((resolve) => {
let settled = false;
let timedOut = false;
let killTimer: NodeJS.Timeout | null = null;
const runtimeTimer = setTimeout(() => {
if (settled) return;
timedOut = true;
const hint = sshRuntimeTimeoutHint({
invocation,
transport: "backend-core-broker",
timeoutMs: runtimeTimeoutMs,
});
const formatted = formatSshRuntimeTimeoutHint(hint);
appendStderrTail(formatted);
process.stderr.write(formatted);
try {
child.stdin.destroy();
} catch {
// Ignore stdin teardown failures on the timeout path.
}
try {
child.kill("SIGTERM");
} catch {
// Ignore kill failures and fall through to finish.
}
killTimer = setTimeout(() => {
try {
child.kill("SIGKILL");
} catch {
// Process may have already exited.
}
}, 2000);
finish(124);
}, runtimeTimeoutMs);
const restore = (): void => {
clearTimeout(runtimeTimer);
if (killTimer && !timedOut) clearTimeout(killTimer);
process.stdin.unpipe(child.stdin);
if (rawMode) process.stdin.setRawMode(false);
};
@@ -2047,7 +2142,7 @@ export async function runSsh(config: UniDeskConfig, providerId: string, args: st
if (settled) return;
settled = true;
restore();
const hint = sshFailureHint(invocation.providerId, parsed, exitCode, stderrTail);
const hint = timedOut ? null : sshFailureHint(invocation.providerId, parsed, exitCode, stderrTail);
if (hint !== null) process.stderr.write(formatSshFailureHint(hint));
const timingHint = formatSshRuntimeTimingHint(sshRuntimeTimingHint({
invocation,