fix: split provider ssh tty modes

This commit is contained in:
Codex
2026-05-07 11:09:16 +00:00
parent 9d8a8e58f3
commit 2aaf0447a6
8 changed files with 28 additions and 15 deletions
+2 -1
View File
@@ -1097,7 +1097,7 @@ function numberFromUnknown(value: unknown, fallback: number, min: number, max: n
async function handleSshClientMessage(ws: ProviderSocket, raw: string | Buffer): Promise<void> {
const text = typeof raw === "string" ? raw : raw.toString("utf8");
const message = JSON.parse(text) as { type?: unknown; providerId?: unknown; cwd?: unknown; command?: unknown; data?: unknown; encoding?: unknown; cols?: unknown; rows?: unknown };
const message = JSON.parse(text) as { type?: unknown; providerId?: unknown; cwd?: unknown; command?: unknown; tty?: unknown; data?: unknown; encoding?: unknown; cols?: unknown; rows?: unknown };
if (message.type === "ssh.open") {
const providerId = typeof message.providerId === "string" ? message.providerId : "";
if (providerId.length === 0) {
@@ -1126,6 +1126,7 @@ async function handleSshClientMessage(ws: ProviderSocket, raw: string | Buffer):
};
if (typeof message.cwd === "string" && message.cwd.length > 0) openMessage.cwd = message.cwd;
if (typeof message.command === "string" && message.command.length > 0) openMessage.command = message.command;
if (typeof message.tty === "boolean") openMessage.tty = message.tty;
provider.send(JSON.stringify(openMessage));
wsSendJson(ws, { type: "ssh.dispatched", providerId, sessionId });
logger("info", "ssh_session_dispatched", { providerId, sessionId, hasCommand: typeof openMessage.command === "string" });
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@unidesk/provider-gateway",
"version": "0.2.7",
"version": "0.2.8",
"private": true,
"type": "module",
"scripts": {
@@ -17,8 +17,12 @@ q_requested=$(quote "$requested_cwd")
q_fallback=$(quote "$fallback_cwd")
q_shell=$(quote "$login_shell")
remote_cmd="cd $q_requested 2>/dev/null || cd $q_fallback 2>/dev/null || cd; export UNIDESK_BRIDGE=host-ssh; exec $q_shell -l"
tty_arg="-T"
if [ -t 0 ] && [ -t 1 ]; then
tty_arg="-tt"
fi
exec ssh -tt \
exec ssh "$tty_arg" \
-i "$key" \
-p "$port" \
-o BatchMode=yes \
+14 -9
View File
@@ -69,6 +69,7 @@ let upgradeSleepTimer: ReturnType<typeof setTimeout> | null = null;
interface HostSshSession {
proc: ReturnType<typeof Bun.spawn>;
openedAt: number;
tty: boolean;
}
interface HostSshStdin {
@@ -963,12 +964,12 @@ async function runHostSsh(payload: Record<string, JsonValue>): Promise<JsonValue
};
}
function hostSshRemoteScript(command: string | null, cwd: string | null, cols?: number, rows?: number): string {
function hostSshRemoteScript(command: string | null, cwd: string | null, allocateTty: boolean, cols?: number, rows?: number): string {
const fallbackCwd = config.hostRemoteCwd ?? `/home/${config.hostSshUser ?? "root"}`;
const requestedCwd = cwd ?? fallbackCwd;
if (command !== null && command.length > 0) assertHostSshCommandAllowed(command, requestedCwd);
const loginShell = config.hostLoginShell ?? "/bin/bash";
const resize = Number.isFinite(cols) && Number.isFinite(rows)
const resize = allocateTty && Number.isFinite(cols) && Number.isFinite(rows)
? `stty rows ${Math.max(8, Math.min(120, Math.floor(rows ?? 30)))} cols ${Math.max(20, Math.min(300, Math.floor(cols ?? 100)))} 2>/dev/null || true`
: "true";
const enterCwd = `cd ${shellQuote(requestedCwd)} 2>/dev/null || cd ${shellQuote(fallbackCwd)} 2>/dev/null || cd`;
@@ -979,7 +980,7 @@ function hostSshRemoteScript(command: string | null, cwd: string | null, cols?:
return `${enterCwd}; ${exports}; ${resize}; ${execPart}`;
}
function hostSshArgs(remoteScript: string): string[] {
function hostSshArgs(remoteScript: string, allocateTty: boolean): string[] {
if (!isHostSshConfigured()) {
throw new Error(`host SSH bridge is not configured; missing ${missingHostSshFields().join(", ")}`);
}
@@ -988,7 +989,7 @@ function hostSshArgs(remoteScript: string): string[] {
throw new Error(`host SSH key is not mounted at ${key}`);
}
return [
"-tt",
allocateTty ? "-tt" : "-T",
"-i",
key,
"-p",
@@ -1044,13 +1045,15 @@ function startHostSshSession(message: CoreHostSshOpenMessage): void {
return;
}
try {
const remoteScript = hostSshRemoteScript(message.command ?? null, message.cwd ?? null, message.cols, message.rows);
const proc = Bun.spawn(["ssh", ...hostSshArgs(remoteScript)], {
const command = typeof message.command === "string" && message.command.length > 0 ? message.command : null;
const allocateTty = typeof message.tty === "boolean" ? message.tty : command === null;
const remoteScript = hostSshRemoteScript(command, message.cwd ?? null, allocateTty, message.cols, message.rows);
const proc = Bun.spawn(["ssh", ...hostSshArgs(remoteScript, allocateTty)], {
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
hostSshSessions.set(message.sessionId, { proc, openedAt: Date.now() });
hostSshSessions.set(message.sessionId, { proc, openedAt: Date.now(), tty: allocateTty });
sendJson({
type: "host_ssh_opened",
providerId: config.providerId,
@@ -1075,7 +1078,7 @@ function startHostSshSession(message: CoreHostSshOpenMessage): void {
hostSshSessions.delete(message.sessionId);
sendHostSshError(message.sessionId, error);
});
logger("info", "host_ssh_session_started", { sessionId: message.sessionId, hasCommand: typeof message.command === "string", cwd: message.cwd ?? null });
logger("info", "host_ssh_session_started", { sessionId: message.sessionId, hasCommand: command !== null, tty: allocateTty, cwd: message.cwd ?? null });
} catch (error) {
sendHostSshError(message.sessionId, error);
}
@@ -1090,7 +1093,9 @@ function writeHostSshInput(message: CoreHostSshInputMessage): void {
try {
const stdin = session.proc.stdin as HostSshStdin | undefined;
if (stdin === undefined) throw new Error("host SSH stdin is not available");
stdin.write(Buffer.from(message.data, "base64"));
const chunk = Buffer.from(message.data, "base64");
if (!session.tty && chunk.length === 1 && chunk[0] === 4) return;
stdin.write(chunk);
} catch (error) {
sendHostSshError(message.sessionId, error);
}
+1
View File
@@ -143,6 +143,7 @@ export interface CoreHostSshOpenMessage {
sessionId: string;
cwd?: string;
command?: string;
tty?: boolean;
cols?: number;
rows?: number;
}