mirror of
https://github.com/andwati/dokploy.git
synced 2026-10-18 03:02:56 +00:00
Merge pull request #5104 from Dokploy/fix/issue-5092-terminal-resize-desync
fix: sync terminal size with backend PTY (fixes #5092)
This commit is contained in:
@@ -26,6 +26,11 @@ interface Props {
|
|||||||
serviceId?: string;
|
serviceId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sentinel the container-picker views fall back to before a real container
|
||||||
|
// is selected/auto-selected — querying logs for it just surfaces Docker's
|
||||||
|
// raw "No such container: select-a-container" daemon error.
|
||||||
|
const PLACEHOLDER_CONTAINER_ID = "select-a-container";
|
||||||
|
|
||||||
export const priorities = [
|
export const priorities = [
|
||||||
{
|
{
|
||||||
label: "Info",
|
label: "Info",
|
||||||
@@ -55,13 +60,16 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
runType,
|
runType,
|
||||||
serviceId,
|
serviceId,
|
||||||
}) => {
|
}) => {
|
||||||
|
const hasContainer =
|
||||||
|
!!containerId && containerId !== PLACEHOLDER_CONTAINER_ID;
|
||||||
|
|
||||||
const { data } = api.docker.getConfig.useQuery(
|
const { data } = api.docker.getConfig.useQuery(
|
||||||
{
|
{
|
||||||
containerId,
|
containerId,
|
||||||
serverId: serverId ?? undefined,
|
serverId: serverId ?? undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!containerId,
|
enabled: hasContainer,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -134,7 +142,7 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerId) return;
|
if (!hasContainer) return;
|
||||||
|
|
||||||
let isCurrentConnection = true;
|
let isCurrentConnection = true;
|
||||||
let noDataTimeout: NodeJS.Timeout;
|
let noDataTimeout: NodeJS.Timeout;
|
||||||
@@ -425,10 +433,15 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
<div className="flex justify-center items-center h-full text-muted-foreground">
|
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||||
<Loader2 className="h-6 w-6 animate-spin" />
|
<Loader2 className="h-6 w-6 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : hasContainer ? (
|
||||||
<div className="flex justify-center items-center h-full text-muted-foreground">
|
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||||
No logs found
|
No logs found
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-center items-center h-full text-center text-sm text-muted-foreground px-8">
|
||||||
|
Select a container above to view its logs. If none are listed,
|
||||||
|
make sure the service is deployed and running.
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ interface Props {
|
|||||||
serviceId?: string;
|
serviceId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sentinel the container-picker modal renders with before a real container
|
||||||
|
// is selected/auto-selected — never worth opening a connection for.
|
||||||
|
const PLACEHOLDER_CONTAINER_ID = "select-a-container";
|
||||||
|
|
||||||
export const DockerTerminal: React.FC<Props> = ({
|
export const DockerTerminal: React.FC<Props> = ({
|
||||||
id,
|
id,
|
||||||
containerId,
|
containerId,
|
||||||
@@ -25,59 +29,105 @@ export const DockerTerminal: React.FC<Props> = ({
|
|||||||
const [activeWay, setActiveWay] = React.useState<string | undefined>("bash");
|
const [activeWay, setActiveWay] = React.useState<string | undefined>("bash");
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = document.getElementById(id);
|
if (!containerId || containerId === PLACEHOLDER_CONTAINER_ID) {
|
||||||
if (container) {
|
return;
|
||||||
container.innerHTML = "";
|
|
||||||
}
|
}
|
||||||
const term = new Terminal({
|
|
||||||
cursorBlink: true,
|
let cancelled = false;
|
||||||
lineHeight: 1.4,
|
let term: Terminal | null = null;
|
||||||
convertEol: true,
|
let ws: WebSocket | null = null;
|
||||||
theme: {
|
let resizeObserver: ResizeObserver | null = null;
|
||||||
cursor: resolvedTheme === "light" ? "#000000" : "transparent",
|
|
||||||
background: "rgba(0, 0, 0, 0)",
|
// Deferred a frame so React StrictMode's dev-only phantom mount+cleanup
|
||||||
foreground: "currentColor",
|
// (which runs synchronously, before anything paints) never creates a
|
||||||
},
|
// terminal in the first place — dodges a known xterm.js dispose race
|
||||||
|
// (xtermjs/xterm.js#5011) where a deferred internal callback outlives
|
||||||
|
// term.dispose() and reads renderer state that's already gone.
|
||||||
|
const frame = requestAnimationFrame(() => {
|
||||||
|
if (cancelled) return;
|
||||||
|
|
||||||
|
const container = document.getElementById(id);
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = "";
|
||||||
|
}
|
||||||
|
term = new Terminal({
|
||||||
|
cursorBlink: true,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
convertEol: true,
|
||||||
|
theme: {
|
||||||
|
cursor: resolvedTheme === "light" ? "#000000" : "transparent",
|
||||||
|
background: "rgba(0, 0, 0, 0)",
|
||||||
|
foreground: "currentColor",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const addonFit = new FitAddon();
|
||||||
|
const clipboardAddon = new ClipboardAddon();
|
||||||
|
term.loadAddon(clipboardAddon);
|
||||||
|
fixMacOsAltKeys(term);
|
||||||
|
// @ts-expect-error
|
||||||
|
term.open(termRef.current);
|
||||||
|
term.loadAddon(addonFit);
|
||||||
|
addonFit.fit();
|
||||||
|
|
||||||
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}&cols=${term.cols}&rows=${term.rows}${serverId ? `&serverId=${serverId}` : ""}${serviceId ? `&serviceId=${serviceId}` : ""}`;
|
||||||
|
|
||||||
|
ws = new WebSocket(wsUrl);
|
||||||
|
const addonAttach = new AttachAddon(ws);
|
||||||
|
term.loadAddon(addonAttach);
|
||||||
|
|
||||||
|
const sendResize = (cols: number, rows: number) => {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({ type: "resize", cols, rows }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
term.onResize(({ cols, rows }) => sendResize(cols, rows));
|
||||||
|
|
||||||
|
resizeObserver = new ResizeObserver(() => addonFit.fit());
|
||||||
|
if (termRef.current) {
|
||||||
|
resizeObserver.observe(termRef.current);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const addonFit = new FitAddon();
|
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
||||||
|
|
||||||
const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}${serverId ? `&serverId=${serverId}` : ""}${serviceId ? `&serviceId=${serviceId}` : ""}`;
|
|
||||||
|
|
||||||
const ws = new WebSocket(wsUrl);
|
|
||||||
|
|
||||||
const addonAttach = new AttachAddon(ws);
|
|
||||||
const clipboardAddon = new ClipboardAddon();
|
|
||||||
term.loadAddon(clipboardAddon);
|
|
||||||
fixMacOsAltKeys(term);
|
|
||||||
// @ts-ignore
|
|
||||||
term.open(termRef.current);
|
|
||||||
// @ts-ignore
|
|
||||||
term.loadAddon(addonFit);
|
|
||||||
term.loadAddon(addonAttach);
|
|
||||||
addonFit.fit();
|
|
||||||
return () => {
|
return () => {
|
||||||
ws.readyState === WebSocket.OPEN && ws.close();
|
cancelled = true;
|
||||||
term.dispose();
|
cancelAnimationFrame(frame);
|
||||||
|
resizeObserver?.disconnect();
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
term?.dispose();
|
||||||
};
|
};
|
||||||
}, [containerId, activeWay, id]);
|
}, [containerId, activeWay, id]);
|
||||||
|
|
||||||
|
const hasContainer =
|
||||||
|
!!containerId && containerId !== PLACEHOLDER_CONTAINER_ID;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-2 mt-4">
|
{hasContainer && (
|
||||||
<span>
|
<div className="flex flex-col gap-2 mt-4">
|
||||||
Select way to connect to <b>{containerId}</b>
|
<span>
|
||||||
</span>
|
Select way to connect to <b>{containerId}</b>
|
||||||
<Tabs value={activeWay} onValueChange={setActiveWay}>
|
</span>
|
||||||
<TabsList>
|
<Tabs value={activeWay} onValueChange={setActiveWay}>
|
||||||
<TabsTrigger value="bash">Bash</TabsTrigger>
|
<TabsList>
|
||||||
<TabsTrigger value="sh">/bin/sh</TabsTrigger>
|
<TabsTrigger value="bash">Bash</TabsTrigger>
|
||||||
</TabsList>
|
<TabsTrigger value="sh">/bin/sh</TabsTrigger>
|
||||||
</Tabs>
|
</TabsList>
|
||||||
</div>
|
</Tabs>
|
||||||
<div className="w-full h-full rounded-lg p-2 bg-transparent border">
|
</div>
|
||||||
<div id={id} ref={termRef} />
|
)}
|
||||||
</div>
|
{hasContainer ? (
|
||||||
|
<div className="w-full h-[420px] rounded-lg p-2 bg-transparent border">
|
||||||
|
<div id={id} ref={termRef} className="h-full" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-[420px] w-full items-center justify-center rounded-lg border border-dashed p-4 text-center text-sm text-muted-foreground">
|
||||||
|
Select a container above to open a terminal. If none are listed, make
|
||||||
|
sure the service is deployed and running.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,11 +41,22 @@ export const Terminal: React.FC<Props> = ({ id, serverId }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const addonFit = new FitAddon();
|
const addonFit = new FitAddon();
|
||||||
|
const clipboardAddon = new ClipboardAddon();
|
||||||
|
term.loadAddon(clipboardAddon);
|
||||||
|
fixMacOsAltKeys(term);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
term.open(termRef.current);
|
||||||
|
// @ts-ignore
|
||||||
|
term.loadAddon(addonFit);
|
||||||
|
addonFit.fit();
|
||||||
|
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
|
||||||
const urlParams = new URLSearchParams();
|
const urlParams = new URLSearchParams();
|
||||||
urlParams.set("serverId", serverId);
|
urlParams.set("serverId", serverId);
|
||||||
|
urlParams.set("cols", term.cols.toString());
|
||||||
|
urlParams.set("rows", term.rows.toString());
|
||||||
|
|
||||||
if (serverId === "local") {
|
if (serverId === "local") {
|
||||||
const { port, username } = getLocalServerData();
|
const { port, username } = getLocalServerData();
|
||||||
@@ -57,17 +68,22 @@ export const Terminal: React.FC<Props> = ({ id, serverId }) => {
|
|||||||
|
|
||||||
const ws = new WebSocket(wsUrl);
|
const ws = new WebSocket(wsUrl);
|
||||||
const addonAttach = new AttachAddon(ws);
|
const addonAttach = new AttachAddon(ws);
|
||||||
const clipboardAddon = new ClipboardAddon();
|
|
||||||
term.loadAddon(clipboardAddon);
|
|
||||||
fixMacOsAltKeys(term);
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
term.open(termRef.current);
|
|
||||||
// @ts-ignore
|
|
||||||
term.loadAddon(addonFit);
|
|
||||||
term.loadAddon(addonAttach);
|
term.loadAddon(addonAttach);
|
||||||
addonFit.fit();
|
|
||||||
|
const sendResize = (cols: number, rows: number) => {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({ type: "resize", cols, rows }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
term.onResize(({ cols, rows }) => sendResize(cols, rows));
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(() => addonFit.fit());
|
||||||
|
if (termRef.current) {
|
||||||
|
resizeObserver.observe(termRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
ws.readyState === WebSocket.OPEN && ws.close();
|
ws.readyState === WebSocket.OPEN && ws.close();
|
||||||
};
|
};
|
||||||
}, [id, serverId]);
|
}, [id, serverId]);
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import { spawn } from "node-pty";
|
|||||||
import { Client } from "ssh2";
|
import { Client } from "ssh2";
|
||||||
import { WebSocketServer } from "ws";
|
import { WebSocketServer } from "ws";
|
||||||
import { canAccessDockerOverWss } from "./authorize";
|
import { canAccessDockerOverWss } from "./authorize";
|
||||||
import { isValidContainerId, isValidShell } from "./utils";
|
import {
|
||||||
|
isValidContainerId,
|
||||||
|
isValidShell,
|
||||||
|
parseResizeMessage,
|
||||||
|
parseTerminalSize,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
export const setupDockerContainerTerminalWebSocketServer = (
|
export const setupDockerContainerTerminalWebSocketServer = (
|
||||||
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||||
@@ -34,6 +39,10 @@ export const setupDockerContainerTerminalWebSocketServer = (
|
|||||||
const activeWay = url.searchParams.get("activeWay");
|
const activeWay = url.searchParams.get("activeWay");
|
||||||
const serverId = url.searchParams.get("serverId");
|
const serverId = url.searchParams.get("serverId");
|
||||||
const serviceId = url.searchParams.get("serviceId");
|
const serviceId = url.searchParams.get("serviceId");
|
||||||
|
const { cols, rows } = parseTerminalSize(
|
||||||
|
url.searchParams.get("cols"),
|
||||||
|
url.searchParams.get("rows"),
|
||||||
|
);
|
||||||
const { user, session } = await validateRequest(req);
|
const { user, session } = await validateRequest(req);
|
||||||
|
|
||||||
if (!containerId) {
|
if (!containerId) {
|
||||||
@@ -92,7 +101,7 @@ export const setupDockerContainerTerminalWebSocketServer = (
|
|||||||
containerId,
|
containerId,
|
||||||
shell,
|
shell,
|
||||||
].join(" ");
|
].join(" ");
|
||||||
conn.exec(dockerCommand, { pty: true }, (err, stream) => {
|
conn.exec(dockerCommand, { pty: { cols, rows } }, (err, stream) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("SSH exec error:", err);
|
console.error("SSH exec error:", err);
|
||||||
ws.close();
|
ws.close();
|
||||||
@@ -123,7 +132,13 @@ export const setupDockerContainerTerminalWebSocketServer = (
|
|||||||
} else {
|
} else {
|
||||||
command = message;
|
command = message;
|
||||||
}
|
}
|
||||||
stream.write(command.toString());
|
const text = command.toString();
|
||||||
|
const resize = parseResizeMessage(text);
|
||||||
|
if (resize) {
|
||||||
|
stream.setWindow(resize.rows, resize.cols, 0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stream.write(text);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const errorMessage = error?.message as unknown as string;
|
const errorMessage = error?.message as unknown as string;
|
||||||
@@ -161,7 +176,7 @@ export const setupDockerContainerTerminalWebSocketServer = (
|
|||||||
const ptyProcess = spawn(
|
const ptyProcess = spawn(
|
||||||
"docker",
|
"docker",
|
||||||
["exec", "-it", "-w", "/", containerId, shell],
|
["exec", "-it", "-w", "/", containerId, shell],
|
||||||
{},
|
{ cols, rows },
|
||||||
);
|
);
|
||||||
|
|
||||||
ptyProcess.onData((data) => {
|
ptyProcess.onData((data) => {
|
||||||
@@ -178,7 +193,13 @@ export const setupDockerContainerTerminalWebSocketServer = (
|
|||||||
} else {
|
} else {
|
||||||
command = message;
|
command = message;
|
||||||
}
|
}
|
||||||
ptyProcess.write(command.toString());
|
const text = command.toString();
|
||||||
|
const resize = parseResizeMessage(text);
|
||||||
|
if (resize) {
|
||||||
|
ptyProcess.resize(resize.cols, resize.rows);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ptyProcess.write(text);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const errorMessage = error?.message as unknown as string;
|
const errorMessage = error?.message as unknown as string;
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ import { Client, type ConnectConfig } from "ssh2";
|
|||||||
import { WebSocketServer } from "ws";
|
import { WebSocketServer } from "ws";
|
||||||
import { getDockerHost } from "../utils/docker";
|
import { getDockerHost } from "../utils/docker";
|
||||||
import { canAccessTerminalOverWss } from "./authorize";
|
import { canAccessTerminalOverWss } from "./authorize";
|
||||||
import { setupLocalServerSSHKey } from "./utils";
|
import {
|
||||||
|
parseResizeMessage,
|
||||||
|
parseTerminalSize,
|
||||||
|
setupLocalServerSSHKey,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
const COMMAND_TO_ALLOW_LOCAL_ACCESS = `
|
const COMMAND_TO_ALLOW_LOCAL_ACCESS = `
|
||||||
# ----------------------------------------
|
# ----------------------------------------
|
||||||
@@ -88,6 +92,10 @@ export const setupTerminalWebSocketServer = (
|
|||||||
wssTerm.on("connection", async (ws, req) => {
|
wssTerm.on("connection", async (ws, req) => {
|
||||||
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
||||||
const serverId = url.searchParams.get("serverId");
|
const serverId = url.searchParams.get("serverId");
|
||||||
|
const { cols, rows } = parseTerminalSize(
|
||||||
|
url.searchParams.get("cols"),
|
||||||
|
url.searchParams.get("rows"),
|
||||||
|
);
|
||||||
const { user, session } = await validateRequest(req);
|
const { user, session } = await validateRequest(req);
|
||||||
if (!user || !session || !serverId) {
|
if (!user || !session || !serverId) {
|
||||||
ws.close();
|
ws.close();
|
||||||
@@ -190,7 +198,7 @@ export const setupTerminalWebSocketServer = (
|
|||||||
// Clear terminal content once connected
|
// Clear terminal content once connected
|
||||||
ws.send("\x1bc");
|
ws.send("\x1bc");
|
||||||
|
|
||||||
conn.shell({}, (err, stream) => {
|
conn.shell({ cols, rows }, (err, stream) => {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
|
|
||||||
stream
|
stream
|
||||||
@@ -216,7 +224,13 @@ export const setupTerminalWebSocketServer = (
|
|||||||
} else {
|
} else {
|
||||||
command = message;
|
command = message;
|
||||||
}
|
}
|
||||||
stream.write(command.toString());
|
const text = command.toString();
|
||||||
|
const resize = parseResizeMessage(text);
|
||||||
|
if (resize) {
|
||||||
|
stream.setWindow(resize.rows, resize.cols, 0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stream.write(text);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const errorMessage = error?.message as unknown as string;
|
const errorMessage = error?.message as unknown as string;
|
||||||
|
|||||||
@@ -63,6 +63,48 @@ export const isValidShell = (shell: string): boolean => {
|
|||||||
return allowedShells.includes(shell);
|
return allowedShells.includes(shell);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clamps cols/rows read from the client's initial connection query params
|
||||||
|
* to a sane range, falling back to the standard 80x24 default.
|
||||||
|
*/
|
||||||
|
export const parseTerminalSize = (
|
||||||
|
colsParam: string | null,
|
||||||
|
rowsParam: string | null,
|
||||||
|
) => {
|
||||||
|
const cols = Number(colsParam);
|
||||||
|
const rows = Number(rowsParam);
|
||||||
|
return {
|
||||||
|
cols: Number.isInteger(cols) && cols > 0 && cols <= 1000 ? cols : 80,
|
||||||
|
rows: Number.isInteger(rows) && rows > 0 && rows <= 1000 ? rows : 24,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Terminal input and resize control messages share the same websocket
|
||||||
|
* channel. Resize messages are JSON envelopes; regular keystrokes never
|
||||||
|
* start with "{", so this distinguishes them without an extra channel.
|
||||||
|
*/
|
||||||
|
export const parseResizeMessage = (data: string) => {
|
||||||
|
if (!data.startsWith("{")) return null;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(data);
|
||||||
|
if (
|
||||||
|
parsed?.type === "resize" &&
|
||||||
|
Number.isInteger(parsed.cols) &&
|
||||||
|
Number.isInteger(parsed.rows) &&
|
||||||
|
parsed.cols > 0 &&
|
||||||
|
parsed.cols <= 1000 &&
|
||||||
|
parsed.rows > 0 &&
|
||||||
|
parsed.rows <= 1000
|
||||||
|
) {
|
||||||
|
return { cols: parsed.cols, rows: parsed.rows };
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
export const getShell = () => {
|
export const getShell = () => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return "NO_AVAILABLE";
|
return "NO_AVAILABLE";
|
||||||
|
|||||||
Reference in New Issue
Block a user