mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-23 14:36:41 +00:00
Fix Guacamole tab visibility lifecycle (#1074)
Co-authored-by: default-student <default-student@github.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
This commit is contained in:
co-authored by
default-student
Luke Gustafson
parent
08825c256d
commit
9b7f52b629
+1
-1
@@ -14,7 +14,7 @@
|
||||
"format:check": "prettier --check .",
|
||||
"biome:check": "biome check biome.json package.json",
|
||||
"biome:fix": "biome check --write biome.json package.json",
|
||||
"postinstall": "node scripts/patch-app-builder-lib.cjs && node scripts/patch-guacamole-lite.cjs && node scripts/patch-better-sqlite3.cjs && node scripts/patch-nan.cjs && node scripts/patch-xterm-android-ime.cjs",
|
||||
"postinstall": "node scripts/patch-app-builder-lib.cjs && node scripts/patch-guacamole-lite.cjs && node scripts/patch-guacamole-common-js.cjs && node scripts/patch-better-sqlite3.cjs && node scripts/patch-nan.cjs && node scripts/patch-xterm-android-ime.cjs",
|
||||
"prebuild": "node scripts/write-electron-build-info.cjs",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint --fix .",
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const packageRoot = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"node_modules",
|
||||
"guacamole-common-js",
|
||||
);
|
||||
|
||||
const bundlePaths = [
|
||||
path.join(packageRoot, "dist", "esm", "guacamole-common.js"),
|
||||
path.join(packageRoot, "dist", "cjs", "guacamole-common.js"),
|
||||
];
|
||||
|
||||
const oldFlushBlock =
|
||||
" if (window.requestAnimationFrame && document.hasFocus())\n" +
|
||||
" asyncFlush();\n" +
|
||||
" else\n" +
|
||||
" syncFlush();";
|
||||
|
||||
const newFlushBlock =
|
||||
" // Electron can throttle or skip requestAnimationFrame() for inactive\n" +
|
||||
" // windows/tabs even while guacd is still sending display frames. Flush\n" +
|
||||
" // synchronously so Guacamole connections do not stall while waiting for\n" +
|
||||
" // a frame callback that may never run.\n" +
|
||||
" syncFlush();";
|
||||
|
||||
let patched = false;
|
||||
let foundBundle = false;
|
||||
|
||||
for (const bundlePath of bundlePaths) {
|
||||
if (!fs.existsSync(bundlePath)) {
|
||||
console.log(`[patch-guacamole-common-js] ${bundlePath} not found, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
foundBundle = true;
|
||||
let content = fs.readFileSync(bundlePath, "utf8");
|
||||
if (content.includes(newFlushBlock)) continue;
|
||||
|
||||
if (!content.includes(oldFlushBlock)) {
|
||||
console.log(
|
||||
`[patch-guacamole-common-js] Flush target not found in ${bundlePath}, skipping`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
content = content.replace(oldFlushBlock, newFlushBlock);
|
||||
fs.writeFileSync(bundlePath, content);
|
||||
patched = true;
|
||||
}
|
||||
|
||||
if (!foundBundle) {
|
||||
console.log("[patch-guacamole-common-js] File not found, skipping");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!patched) {
|
||||
console.log("[patch-guacamole-common-js] Already patched");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(
|
||||
"[patch-guacamole-common-js] Patched display flush to avoid Electron requestAnimationFrame stalls",
|
||||
);
|
||||
@@ -170,7 +170,6 @@ const clientOptions = {
|
||||
vnc: {
|
||||
"swap-red-blue": false,
|
||||
cursor: "remote",
|
||||
security: "any",
|
||||
width: 1280,
|
||||
height: 720,
|
||||
},
|
||||
|
||||
@@ -589,7 +589,8 @@ router.post(
|
||||
? { guacdPort: perConnectionGuacdPort }
|
||||
: {}),
|
||||
};
|
||||
const recordingEnabled = host.enableSessionLogging !== false;
|
||||
const recordingEnabled =
|
||||
connectionType !== "vnc" && host.enableSessionLogging !== false;
|
||||
const recordingName = `${crypto.randomUUID()}.guac`;
|
||||
const recordingPath =
|
||||
process.env.GUACD_RECORDING_PATH ||
|
||||
@@ -657,7 +658,6 @@ router.post(
|
||||
password,
|
||||
{
|
||||
port,
|
||||
security: "any",
|
||||
...guacConfig,
|
||||
...guacdOverrides,
|
||||
},
|
||||
|
||||
@@ -39,6 +39,7 @@ interface GuacamoleAppProps {
|
||||
hostId?: string;
|
||||
tabId?: string;
|
||||
protocol?: "rdp" | "vnc" | "telnet";
|
||||
isVisible?: boolean;
|
||||
}
|
||||
|
||||
export interface GuacamoleAppHandle {
|
||||
@@ -49,7 +50,7 @@ export interface GuacamoleAppHandle {
|
||||
}
|
||||
|
||||
const GuacamoleApp = React.forwardRef<GuacamoleAppHandle, GuacamoleAppProps>(
|
||||
function GuacamoleApp({ hostId, tabId, protocol }, ref) {
|
||||
function GuacamoleApp({ hostId, tabId, protocol, isVisible = true }, ref) {
|
||||
const { t } = useTranslation();
|
||||
const [hostConfig, setHostConfig] = useState<SSHHost | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -103,6 +104,7 @@ const GuacamoleApp = React.forwardRef<GuacamoleAppHandle, GuacamoleAppProps>(
|
||||
hostName={hostConfig.name || hostConfig.ip || String(hostId)}
|
||||
tabId={tabId}
|
||||
protocol={protocol}
|
||||
isVisible={isVisible}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
@@ -118,13 +120,14 @@ interface GuacamoleAppInnerProps {
|
||||
hostName: string;
|
||||
tabId?: string;
|
||||
protocol?: "rdp" | "vnc" | "telnet";
|
||||
isVisible: boolean;
|
||||
}
|
||||
|
||||
const GuacamoleAppInner = React.forwardRef<
|
||||
GuacamoleAppHandle,
|
||||
GuacamoleAppInnerProps
|
||||
>(function GuacamoleAppInner(
|
||||
{ hostId, hostConfig, hostName, tabId, protocol },
|
||||
{ hostId, hostConfig, hostName, tabId, protocol, isVisible },
|
||||
ref,
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
@@ -402,7 +405,7 @@ const GuacamoleAppInner = React.forwardRef<
|
||||
? configuredDpi
|
||||
: undefined,
|
||||
}}
|
||||
isVisible={true}
|
||||
isVisible={isVisible}
|
||||
touchMode={touchMode}
|
||||
onError={(err) => setConnectionError(err)}
|
||||
/>
|
||||
|
||||
@@ -130,11 +130,11 @@ export const GuacamoleDisplay = forwardRef<
|
||||
},
|
||||
}));
|
||||
|
||||
const getWebSocketUrl = useCallback(
|
||||
const getWebSocketConnection = useCallback(
|
||||
async (
|
||||
containerWidth: number,
|
||||
containerHeight: number,
|
||||
): Promise<string | null> => {
|
||||
): Promise<{ url: string; query: string } | null> => {
|
||||
try {
|
||||
let token: string;
|
||||
const connectionProtocol =
|
||||
@@ -205,7 +205,7 @@ export const GuacamoleDisplay = forwardRef<
|
||||
height: String(displaySize.height),
|
||||
});
|
||||
if (displaySize.dpi) params.set("dpi", String(displaySize.dpi));
|
||||
return `${wsBase}?${params.toString()}`;
|
||||
return { url: wsBase, query: params.toString() };
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error";
|
||||
@@ -308,10 +308,9 @@ export const GuacamoleDisplay = forwardRef<
|
||||
setIsReady(false);
|
||||
setHasError(false);
|
||||
|
||||
// Wait two frames so the container is fully laid out before measuring.
|
||||
await new Promise<void>((resolve) =>
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => resolve())),
|
||||
);
|
||||
// Let layout settle before measuring without depending on animation frames,
|
||||
// which may be throttled while Electron windows or tabs are inactive.
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
||||
if (!isMountedRef.current) {
|
||||
isConnectingRef.current = false;
|
||||
return;
|
||||
@@ -333,9 +332,7 @@ export const GuacamoleDisplay = forwardRef<
|
||||
(containerWidth < 100 || containerHeight < 100) && attempt < 40;
|
||||
attempt++
|
||||
) {
|
||||
await new Promise<void>((resolve) =>
|
||||
requestAnimationFrame(() => resolve()),
|
||||
);
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 25));
|
||||
if (!isMountedRef.current) {
|
||||
isConnectingRef.current = false;
|
||||
return;
|
||||
@@ -348,19 +345,28 @@ export const GuacamoleDisplay = forwardRef<
|
||||
containerHeight = window.innerHeight || 720;
|
||||
}
|
||||
|
||||
const wsUrl = await getWebSocketUrl(containerWidth, containerHeight);
|
||||
const wsConnection = await getWebSocketConnection(
|
||||
containerWidth,
|
||||
containerHeight,
|
||||
);
|
||||
if (!isMountedRef.current) {
|
||||
isConnectingRef.current = false;
|
||||
return;
|
||||
}
|
||||
if (!wsUrl) {
|
||||
if (!wsConnection) {
|
||||
isConnectingRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const tunnel = new Guacamole.WebSocketTunnel(wsUrl);
|
||||
const tunnel = new Guacamole.WebSocketTunnel(wsConnection.url);
|
||||
const client = new Guacamole.Client(tunnel);
|
||||
clientRef.current = client;
|
||||
let connectWatchdog: ReturnType<typeof setTimeout> | null = null;
|
||||
const clearConnectWatchdog = () => {
|
||||
if (!connectWatchdog) return;
|
||||
clearTimeout(connectWatchdog);
|
||||
connectWatchdog = null;
|
||||
};
|
||||
|
||||
const display = client.getDisplay();
|
||||
const displayElement = display.getElement();
|
||||
@@ -401,7 +407,7 @@ export const GuacamoleDisplay = forwardRef<
|
||||
}
|
||||
|
||||
display.onresize = () => {
|
||||
if (!isMountedRef.current) return;
|
||||
if (!isMountedRef.current || clientRef.current !== client) return;
|
||||
rescaleDisplay(true);
|
||||
setIsReady(true);
|
||||
};
|
||||
@@ -474,7 +480,7 @@ export const GuacamoleDisplay = forwardRef<
|
||||
refreshKeyboardHandlers();
|
||||
|
||||
client.onstatechange = (state: number) => {
|
||||
if (!isMountedRef.current) return;
|
||||
if (!isMountedRef.current || clientRef.current !== client) return;
|
||||
switch (state) {
|
||||
case 0:
|
||||
break;
|
||||
@@ -483,6 +489,7 @@ export const GuacamoleDisplay = forwardRef<
|
||||
case 2:
|
||||
break;
|
||||
case 3:
|
||||
clearConnectWatchdog();
|
||||
isConnectingRef.current = false;
|
||||
setIsReady(true);
|
||||
onConnect?.();
|
||||
@@ -502,16 +509,21 @@ export const GuacamoleDisplay = forwardRef<
|
||||
case 4:
|
||||
break;
|
||||
case 5:
|
||||
clearConnectWatchdog();
|
||||
isConnectingRef.current = false;
|
||||
setIsReady(false);
|
||||
setHasError(true);
|
||||
hasKeyboardFocusRef.current = false;
|
||||
refreshKeyboardHandlers();
|
||||
onError?.(t("guacamole.connectionError"));
|
||||
onDisconnect?.();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
client.onerror = (error: Guacamole.Status) => {
|
||||
if (!isMountedRef.current) return;
|
||||
if (!isMountedRef.current || clientRef.current !== client) return;
|
||||
clearConnectWatchdog();
|
||||
const errorMessage = error.message || t("guacamole.connectionError");
|
||||
setIsReady(false);
|
||||
setHasError(true);
|
||||
@@ -555,8 +567,21 @@ export const GuacamoleDisplay = forwardRef<
|
||||
};
|
||||
|
||||
try {
|
||||
client.connect();
|
||||
connectWatchdog = setTimeout(() => {
|
||||
if (
|
||||
!isMountedRef.current ||
|
||||
clientRef.current !== client ||
|
||||
!isConnectingRef.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
disconnectClient();
|
||||
void connect();
|
||||
}, 8000);
|
||||
client.connect(wsConnection.query);
|
||||
} catch (error) {
|
||||
clearConnectWatchdog();
|
||||
isConnectingRef.current = false;
|
||||
if (!isMountedRef.current) return;
|
||||
setIsReady(false);
|
||||
@@ -566,12 +591,13 @@ export const GuacamoleDisplay = forwardRef<
|
||||
);
|
||||
}
|
||||
}, [
|
||||
getWebSocketUrl,
|
||||
getWebSocketConnection,
|
||||
onConnect,
|
||||
onDisconnect,
|
||||
onError,
|
||||
refreshKeyboardHandlers,
|
||||
rescaleDisplay,
|
||||
disconnectClient,
|
||||
connectionConfig.protocol,
|
||||
connectionConfig.type,
|
||||
connectionConfig.dpi,
|
||||
|
||||
@@ -390,6 +390,7 @@ export function renderTabContent(
|
||||
hostId={host.id}
|
||||
tabId={tab.id}
|
||||
protocol={tab.type as "rdp" | "vnc" | "telnet"}
|
||||
isVisible={isVisible}
|
||||
/>,
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user