feat: add provider ssh bridge

This commit is contained in:
Codex
2026-05-05 01:24:32 +00:00
parent 2d6829ed44
commit f6d0bd1e3b
18 changed files with 1014 additions and 34 deletions
+186
View File
@@ -0,0 +1,186 @@
import { spawn } from "node:child_process";
import { type UniDeskConfig, repoRoot } from "./config";
interface ParsedSshArgs {
remoteCommand: string | null;
}
const sshOptionsWithValue = new Set([
"-B", "-b", "-c", "-D", "-E", "-e", "-F", "-I", "-i", "-J", "-L", "-l", "-m", "-O", "-o", "-p", "-Q", "-R", "-S", "-W", "-w",
]);
function parseSshArgs(args: string[]): ParsedSshArgs {
const remote: string[] = [];
let remoteStarted = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (remoteStarted) {
remote.push(arg);
continue;
}
if (arg === "--") {
remoteStarted = true;
continue;
}
if (arg.startsWith("-") && arg !== "-") {
if (sshOptionsWithValue.has(arg) && index + 1 < args.length) index += 1;
continue;
}
remoteStarted = true;
remote.push(arg);
}
return { remoteCommand: remote.length === 0 ? null : remote.join(" ") };
}
function brokerSource(): string {
return String.raw`
const open = JSON.parse(process.argv[2] || process.argv[1] || "{}");
const token = process.env.PROVIDER_TOKEN || "";
const url = "ws://127.0.0.1:8080/ws/ssh?token=" + encodeURIComponent(token);
const ws = new WebSocket(url);
let exitCode = 255;
let canSend = false;
let opened = false;
const pending = [];
const openTimer = setTimeout(() => {
if (opened) return;
process.stderr.write("unidesk ssh bridge timed out waiting for provider session\n");
try { ws.close(); } catch {}
process.exit(255);
}, Number(open.openTimeoutMs || 15000));
function send(value) {
const text = JSON.stringify(value);
if (!canSend || ws.readyState !== WebSocket.OPEN) {
pending.push(text);
return;
}
ws.send(text);
}
function flush() {
while (pending.length > 0 && ws.readyState === WebSocket.OPEN) {
ws.send(pending.shift());
}
}
function decodeData(data) {
return typeof data === "string" ? data : Buffer.from(data).toString("utf8");
}
ws.addEventListener("open", () => {
canSend = true;
send({
type: "ssh.open",
providerId: open.providerId,
command: open.command || undefined,
cwd: open.cwd || undefined,
cols: open.cols || 100,
rows: open.rows || 30,
});
flush();
});
ws.addEventListener("message", (event) => {
const message = JSON.parse(decodeData(event.data));
if (message.type === "ssh.data") {
opened = true;
const chunk = Buffer.from(message.data || "", "base64");
if (message.stream === "stderr") process.stderr.write(chunk);
else process.stdout.write(chunk);
return;
}
if (message.type === "ssh.opened") {
opened = true;
clearTimeout(openTimer);
return;
}
if (message.type === "ssh.dispatched") return;
if (message.type === "ssh.error") {
clearTimeout(openTimer);
process.stderr.write(String(message.message || "ssh bridge error") + "\n");
exitCode = 255;
ws.close();
return;
}
if (message.type === "ssh.exit") {
clearTimeout(openTimer);
exitCode = Number.isInteger(message.exitCode) ? message.exitCode : 255;
ws.close();
}
});
ws.addEventListener("close", () => {
process.exit(exitCode);
});
ws.addEventListener("error", () => {
process.stderr.write("unidesk ssh bridge websocket error\n");
process.exit(255);
});
process.stdin.on("data", (chunk) => {
send({ type: "ssh.input", data: Buffer.from(chunk).toString("base64"), encoding: "base64" });
flush();
});
process.stdin.on("end", () => {
send({ type: "ssh.eof" });
flush();
});
`;
}
function terminalSize(): { cols: number; rows: number } {
return {
cols: Number(process.stdout.columns) > 0 ? Number(process.stdout.columns) : 100,
rows: Number(process.stdout.rows) > 0 ? Number(process.stdout.rows) : 30,
};
}
export async function runSsh(config: UniDeskConfig, providerId: string, args: string[]): Promise<number> {
if (!providerId) throw new Error("ssh requires provider id, for example: bun scripts/cli.ts ssh D518");
const parsed = parseSshArgs(args);
const size = terminalSize();
const payload = {
providerId,
command: parsed.remoteCommand,
cols: size.cols,
rows: size.rows,
};
const child = spawn("docker", [
"exec",
"-i",
"unidesk-backend-core",
"bun",
"-e",
brokerSource(),
"--",
JSON.stringify(payload),
], {
cwd: repoRoot,
stdio: ["pipe", "pipe", "pipe"],
});
const rawMode = parsed.remoteCommand === null && process.stdin.isTTY;
if (rawMode) process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.pipe(child.stdin);
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
return await new Promise<number>((resolve) => {
const restore = (): void => {
process.stdin.unpipe(child.stdin);
if (rawMode) process.stdin.setRawMode(false);
};
child.on("error", (error) => {
restore();
process.stderr.write(`unidesk ssh failed to start broker: ${error.message}\n`);
resolve(255);
});
child.on("close", (code) => {
restore();
resolve(code ?? 255);
});
});
}