Skip to content

Commit c472752

Browse files
authored
fix: hidRPC handshake packet should be only sent once (#969)
1 parent 316c2e6 commit c472752

File tree

5 files changed

+151
-63
lines changed

5 files changed

+151
-63
lines changed

ui/package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"react-xtermjs": "^1.0.10",
5757
"recharts": "^3.3.0",
5858
"tailwind-merge": "^3.3.1",
59+
"tslog": "^4.10.2",
5960
"usehooks-ts": "^3.1.1",
6061
"validator": "^13.15.20",
6162
"zustand": "^4.5.2"

ui/src/hooks/useHidRpc.ts

Lines changed: 132 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useCallback, useEffect, useMemo } from "react";
2+
import { Logger } from "tslog";
23

34
import { useRTCStore } from "@hooks/stores";
45

@@ -25,6 +26,128 @@ interface sendMessageParams {
2526
requireOrdered?: boolean;
2627
}
2728

29+
const HANDSHAKE_TIMEOUT = 30 * 1000; // 30 seconds
30+
const HANDSHAKE_MAX_ATTEMPTS = 10;
31+
const logger = new Logger({ name: "hidrpc" });
32+
33+
export function doRpcHidHandshake(rpcHidChannel: RTCDataChannel, setRpcHidProtocolVersion: (version: number | null) => void) {
34+
let attempts = 0;
35+
let lastConnectedTime: Date | undefined;
36+
let lastSendTime: Date | undefined;
37+
let handshakeCompleted = false;
38+
let handshakeInterval: ReturnType<typeof setInterval> | null = null;
39+
40+
const shouldGiveUp = () => {
41+
if (attempts > HANDSHAKE_MAX_ATTEMPTS) {
42+
logger.error(`Failed to send handshake message after ${HANDSHAKE_MAX_ATTEMPTS} attempts`);
43+
return true;
44+
}
45+
46+
const timeSinceConnected = lastConnectedTime ? Date.now() - lastConnectedTime.getTime() : 0;
47+
if (timeSinceConnected > HANDSHAKE_TIMEOUT) {
48+
logger.error(`Handshake timed out after ${timeSinceConnected}ms`);
49+
return true;
50+
}
51+
52+
return false;
53+
}
54+
55+
const sendHandshake = (initial: boolean) => {
56+
if (handshakeCompleted) return;
57+
58+
attempts++;
59+
lastSendTime = new Date();
60+
61+
if (!initial && shouldGiveUp()) {
62+
if (handshakeInterval) {
63+
clearInterval(handshakeInterval);
64+
handshakeInterval = null;
65+
}
66+
return;
67+
}
68+
69+
let data: Uint8Array | undefined;
70+
try {
71+
const message = new HandshakeMessage(HID_RPC_VERSION);
72+
data = message.marshal();
73+
} catch (e) {
74+
logger.error("Failed to marshal message", e);
75+
return;
76+
}
77+
if (!data) return;
78+
rpcHidChannel.send(data as unknown as ArrayBuffer);
79+
80+
if (initial) {
81+
handshakeInterval = setInterval(() => {
82+
sendHandshake(false);
83+
}, 1000);
84+
}
85+
};
86+
87+
const onMessage = (ev: MessageEvent) => {
88+
const message = unmarshalHidRpcMessage(new Uint8Array(ev.data));
89+
if (!message || !(message instanceof HandshakeMessage)) return;
90+
91+
if (!message.version) {
92+
logger.error("Received handshake message without version", message);
93+
return;
94+
}
95+
96+
if (message.version > HID_RPC_VERSION) {
97+
// we assume that the UI is always using the latest version of the HID RPC protocol
98+
// so we can't support this
99+
// TODO: use capabilities to determine rather than version number
100+
logger.error("Server is using a newer version than the client", message);
101+
return;
102+
}
103+
104+
setRpcHidProtocolVersion(message.version);
105+
106+
const timeUsed = lastSendTime ? Date.now() - lastSendTime.getTime() : 0;
107+
logger.info(`Handshake completed in ${timeUsed}ms after ${attempts} attempts (Version: ${message.version} / ${HID_RPC_VERSION})`);
108+
109+
// clean up
110+
rpcHidChannel.removeEventListener("message", onMessage);
111+
resetHandshake({ completed: true });
112+
};
113+
114+
const resetHandshake = ({ lastConnectedTime: newLastConnectedTime, completed }: { lastConnectedTime?: Date | undefined, completed?: boolean }) => {
115+
if (newLastConnectedTime) lastConnectedTime = newLastConnectedTime;
116+
lastSendTime = undefined;
117+
attempts = 0;
118+
if (completed !== undefined) handshakeCompleted = completed;
119+
if (handshakeInterval) {
120+
clearInterval(handshakeInterval);
121+
handshakeInterval = null;
122+
}
123+
};
124+
125+
const onConnected = () => {
126+
resetHandshake({ lastConnectedTime: new Date() });
127+
logger.info("Channel connected");
128+
129+
sendHandshake(true);
130+
rpcHidChannel.addEventListener("message", onMessage);
131+
};
132+
133+
const onClose = () => {
134+
resetHandshake({ lastConnectedTime: undefined, completed: false });
135+
136+
logger.info("Channel closed");
137+
setRpcHidProtocolVersion(null);
138+
139+
rpcHidChannel.removeEventListener("message", onMessage);
140+
};
141+
142+
rpcHidChannel.addEventListener("open", onConnected);
143+
rpcHidChannel.addEventListener("close", onClose);
144+
145+
// handle case where channel is already open when the hook is mounted
146+
if (rpcHidChannel.readyState === "open") {
147+
onConnected();
148+
}
149+
}
150+
28151
export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
29152
const {
30153
rpcHidChannel,
@@ -78,7 +201,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
78201
try {
79202
data = message.marshal();
80203
} catch (e) {
81-
console.error("Failed to marshal HID RPC message", e);
204+
logger.error("Failed to marshal message", e);
82205
}
83206
if (!data) return;
84207

@@ -151,99 +274,46 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
151274
sendMessage(KEEPALIVE_MESSAGE);
152275
}, [sendMessage]);
153276

154-
const sendHandshake = useCallback(() => {
155-
if (hidRpcDisabled) return;
156-
if (rpcHidProtocolVersion) return;
157-
if (!rpcHidChannel) return;
158-
159-
sendMessage(new HandshakeMessage(HID_RPC_VERSION), { ignoreHandshakeState: true });
160-
}, [rpcHidChannel, rpcHidProtocolVersion, sendMessage, hidRpcDisabled]);
161-
162-
const handleHandshake = useCallback(
163-
(message: HandshakeMessage) => {
164-
if (hidRpcDisabled) return;
165-
166-
if (!message.version) {
167-
console.error("Received handshake message without version", message);
168-
return;
169-
}
170-
171-
if (message.version > HID_RPC_VERSION) {
172-
// we assume that the UI is always using the latest version of the HID RPC protocol
173-
// so we can't support this
174-
// TODO: use capabilities to determine rather than version number
175-
console.error("Server is using a newer HID RPC version than the client", message);
176-
return;
177-
}
178-
179-
setRpcHidProtocolVersion(message.version);
180-
},
181-
[setRpcHidProtocolVersion, hidRpcDisabled],
182-
);
183-
184277
useEffect(() => {
185278
if (!rpcHidChannel) return;
186279
if (hidRpcDisabled) return;
187280

188-
// send handshake message
189-
sendHandshake();
190-
191281
const messageHandler = (e: MessageEvent) => {
192282
if (typeof e.data === "string") {
193-
console.warn("Received string data in HID RPC message handler", e.data);
283+
logger.warn("Received string data in message handler", e.data);
194284
return;
195285
}
196286

197287
const message = unmarshalHidRpcMessage(new Uint8Array(e.data));
198288
if (!message) {
199-
console.warn("Received invalid HID RPC message", e.data);
289+
logger.warn("Received invalid message", e.data);
200290
return;
201291
}
202292

203-
console.debug("Received HID RPC message", message);
204-
switch (message.constructor) {
205-
case HandshakeMessage:
206-
handleHandshake(message as HandshakeMessage);
207-
break;
208-
default:
209-
// not all events are handled here, the rest are handled by the onHidRpcMessage callback
210-
break;
211-
}
293+
if (message instanceof HandshakeMessage) return; // handshake message is handled by the doRpcHidHandshake function
212294

213-
onHidRpcMessage?.(message);
214-
};
295+
// to remove it from the production build, we need to use the /* @__PURE__ */ comment here
296+
// setting `esbuild.pure` doesn't work
297+
/* @__PURE__ */ logger.debug("Received message", message);
215298

216-
const openHandler = () => {
217-
console.info("HID RPC channel opened");
218-
sendHandshake();
219-
};
220-
221-
const closeHandler = () => {
222-
console.info("HID RPC channel closed");
223-
setRpcHidProtocolVersion(null);
299+
onHidRpcMessage?.(message);
224300
};
225301

226302
const errorHandler = (e: Event) => {
227-
console.error(`Error on rpcHidChannel '${rpcHidChannel.label}': ${e}`)
303+
logger.error(`Error on channel '${rpcHidChannel.label}'`, e);
228304
};
229305

230306
rpcHidChannel.addEventListener("message", messageHandler);
231-
rpcHidChannel.addEventListener("close", closeHandler);
232307
rpcHidChannel.addEventListener("error", errorHandler);
233-
rpcHidChannel.addEventListener("open", openHandler);
234308

235309
return () => {
236310
rpcHidChannel.removeEventListener("message", messageHandler);
237-
rpcHidChannel.removeEventListener("close", closeHandler);
238311
rpcHidChannel.removeEventListener("error", errorHandler);
239-
rpcHidChannel.removeEventListener("open", openHandler);
240312
};
241313
}, [
242314
rpcHidChannel,
243315
onHidRpcMessage,
244316
setRpcHidProtocolVersion,
245-
sendHandshake,
246-
handleHandshake,
247317
hidRpcDisabled,
248318
]);
249319

ui/src/routes/devices.$id.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {
5353
} from "@components/VideoOverlay";
5454
import { FeatureFlagProvider } from "@providers/FeatureFlagProvider";
5555
import { m } from "@localizations/messages.js";
56+
import { doRpcHidHandshake } from "@hooks/useHidRpc";
5657

5758
export type AuthMode = "password" | "noPassword" | null;
5859

@@ -127,6 +128,7 @@ export default function KvmIdRoute() {
127128
setRpcHidChannel,
128129
setRpcHidUnreliableNonOrderedChannel,
129130
setRpcHidUnreliableChannel,
131+
setRpcHidProtocolVersion,
130132
} = useRTCStore();
131133

132134
const location = useLocation();
@@ -498,6 +500,7 @@ export default function KvmIdRoute() {
498500
rpcHidChannel.onopen = () => {
499501
setRpcHidChannel(rpcHidChannel);
500502
};
503+
doRpcHidHandshake(rpcHidChannel, setRpcHidProtocolVersion);
501504

502505
const rpcHidUnreliableChannel = pc.createDataChannel("hidrpc-unreliable-ordered", {
503506
ordered: true,
@@ -534,6 +537,7 @@ export default function KvmIdRoute() {
534537
setRpcHidChannel,
535538
setRpcHidUnreliableNonOrderedChannel,
536539
setRpcHidUnreliableChannel,
540+
setRpcHidProtocolVersion,
537541
setTransceiver,
538542
]);
539543

ui/vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export default defineConfig(({ mode, command }) => {
3939
return {
4040
plugins,
4141
esbuild: {
42-
pure: ["console.debug"],
42+
pure: command === "build" ? ["console.debug"]: [],
4343
},
4444
assetsInclude: ["**/*.woff2"],
4545
build: {

0 commit comments

Comments
 (0)