mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-12 00:42:06 +08:00
fix(shell): prevent duplicate websocket connects with synchronous lock
The shell connection hook relied on React state (isConnecting/isConnected) as the only guard for connect attempts. Because state updates are asynchronous, rapid connect triggers could race before isConnecting became true and create duplicate WebSocket instances. This change adds a synchronous ref lock (connectingRef) that is checked immediately in connectToShell and connectWebSocket. connectToShell now sets connectingRef.current = true before invoking connectWebSocket so concurrent calls cannot pass between state updates. connectWebSocket now: - returns early when a connection is already locked - sets connectingRef.current = true when creating a socket - clears connectingRef.current alongside setIsConnecting(false) in onopen, onclose, onerror, and catch - clears connectingRef.current when no WebSocket URL is available disconnectFromShell also resets connectingRef to keep lock/state behavior consistent across manual disconnect flows.
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import type { MutableRefObject } from 'react';
|
import type { MutableRefObject } from 'react';
|
||||||
import type { FitAddon } from '@xterm/addon-fit';
|
import type { FitAddon } from '@xterm/addon-fit';
|
||||||
import type { Terminal } from '@xterm/xterm';
|
import type { Terminal } from '@xterm/xterm';
|
||||||
@@ -50,6 +50,7 @@ export function useShellConnection({
|
|||||||
}: UseShellConnectionOptions): UseShellConnectionResult {
|
}: UseShellConnectionOptions): UseShellConnectionResult {
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
const [isConnecting, setIsConnecting] = useState(false);
|
||||||
|
const connectingRef = useRef(false);
|
||||||
|
|
||||||
const handleProcessCompletion = useCallback(
|
const handleProcessCompletion = useCallback(
|
||||||
(output: string) => {
|
(output: string) => {
|
||||||
@@ -101,23 +102,29 @@ export function useShellConnection({
|
|||||||
[handleProcessCompletion, setAuthUrl, terminalRef],
|
[handleProcessCompletion, setAuthUrl, terminalRef],
|
||||||
);
|
);
|
||||||
|
|
||||||
const connectWebSocket = useCallback(() => {
|
const connectWebSocket = useCallback(
|
||||||
if (isConnecting || isConnected) {
|
(isConnectionLocked = false) => {
|
||||||
|
if ((connectingRef.current && !isConnectionLocked) || isConnecting || isConnected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const wsUrl = getShellWebSocketUrl();
|
const wsUrl = getShellWebSocketUrl();
|
||||||
if (!wsUrl) {
|
if (!wsUrl) {
|
||||||
|
connectingRef.current = false;
|
||||||
|
setIsConnecting(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connectingRef.current = true;
|
||||||
|
|
||||||
const socket = new WebSocket(wsUrl);
|
const socket = new WebSocket(wsUrl);
|
||||||
wsRef.current = socket;
|
wsRef.current = socket;
|
||||||
|
|
||||||
socket.onopen = () => {
|
socket.onopen = () => {
|
||||||
setIsConnected(true);
|
setIsConnected(true);
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
|
connectingRef.current = false;
|
||||||
setAuthUrl('');
|
setAuthUrl('');
|
||||||
|
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
@@ -154,18 +161,22 @@ export function useShellConnection({
|
|||||||
socket.onclose = () => {
|
socket.onclose = () => {
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
|
connectingRef.current = false;
|
||||||
clearTerminalScreen();
|
clearTerminalScreen();
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.onerror = () => {
|
socket.onerror = () => {
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
|
connectingRef.current = false;
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
|
connectingRef.current = false;
|
||||||
}
|
}
|
||||||
}, [
|
},
|
||||||
|
[
|
||||||
clearTerminalScreen,
|
clearTerminalScreen,
|
||||||
fitAddonRef,
|
fitAddonRef,
|
||||||
handleSocketMessage,
|
handleSocketMessage,
|
||||||
@@ -178,15 +189,17 @@ export function useShellConnection({
|
|||||||
setAuthUrl,
|
setAuthUrl,
|
||||||
terminalRef,
|
terminalRef,
|
||||||
wsRef,
|
wsRef,
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
|
|
||||||
const connectToShell = useCallback(() => {
|
const connectToShell = useCallback(() => {
|
||||||
if (!isInitialized || isConnected || isConnecting) {
|
if (!isInitialized || isConnected || isConnecting || connectingRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connectingRef.current = true;
|
||||||
setIsConnecting(true);
|
setIsConnecting(true);
|
||||||
connectWebSocket();
|
connectWebSocket(true);
|
||||||
}, [connectWebSocket, isConnected, isConnecting, isInitialized]);
|
}, [connectWebSocket, isConnected, isConnecting, isInitialized]);
|
||||||
|
|
||||||
const disconnectFromShell = useCallback(() => {
|
const disconnectFromShell = useCallback(() => {
|
||||||
@@ -194,6 +207,7 @@ export function useShellConnection({
|
|||||||
clearTerminalScreen();
|
clearTerminalScreen();
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
|
connectingRef.current = false;
|
||||||
setAuthUrl('');
|
setAuthUrl('');
|
||||||
}, [clearTerminalScreen, closeSocket, setAuthUrl]);
|
}, [clearTerminalScreen, closeSocket, setAuthUrl]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user