Skip to content

Commit 0cac863

Browse files
Merge branch 'main' into feature/webrtc-screen-mirror
2 parents dae04b5 + efabeb0 commit 0cac863

4 files changed

Lines changed: 130 additions & 59 deletions

File tree

src/server/InputHandler.ts

Lines changed: 69 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -123,19 +123,23 @@ export class InputHandler {
123123
Number.isFinite(msg.dx) &&
124124
Number.isFinite(msg.dy)
125125
) {
126-
// Attempt ydotool relative movement first
127-
const success = await moveRelative(msg.dx, msg.dy)
128-
129-
// Fallback to absolute positioning if ydotool is unavailable or fails
130-
if (!success) {
131-
const currentPos = await mouse.getPosition()
132-
133-
await mouse.setPosition(
134-
new Point(
135-
Math.round(currentPos.x + msg.dx),
136-
Math.round(currentPos.y + msg.dy),
137-
),
138-
)
126+
try {
127+
// Attempt ydotool relative movement first
128+
const success = await moveRelative(msg.dx, msg.dy)
129+
130+
// Fallback to absolute positioning if ydotool is unavailable or fails
131+
if (!success) {
132+
const currentPos = await mouse.getPosition()
133+
134+
await mouse.setPosition(
135+
new Point(
136+
Math.round(currentPos.x + msg.dx),
137+
Math.round(currentPos.y + msg.dy),
138+
),
139+
)
140+
}
141+
} catch (err) {
142+
console.error("Move event failed:", err)
139143
}
140144
}
141145
break
@@ -150,10 +154,16 @@ export class InputHandler {
150154
? Button.RIGHT
151155
: Button.MIDDLE
152156

153-
if (msg.press) {
154-
await mouse.pressButton(btn)
155-
} else {
156-
await mouse.releaseButton(btn)
157+
try {
158+
if (msg.press) {
159+
await mouse.pressButton(btn)
160+
} else {
161+
await mouse.releaseButton(btn)
162+
}
163+
} catch (err) {
164+
console.error("Click event failed:", err)
165+
// ensure release just in case
166+
await mouse.releaseButton(btn).catch(() => {})
157167
}
158168
}
159169
break
@@ -188,30 +198,35 @@ export class InputHandler {
188198

189199
case "scroll": {
190200
const MAX_SCROLL = 100
191-
const promises: Promise<void>[] = []
201+
const promises: Promise<unknown>[] = []
192202

193203
// Vertical scroll
194204
if (this.isFiniteNumber(msg.dy) && Math.round(msg.dy) !== 0) {
195205
const amount = this.clamp(Math.round(msg.dy), -MAX_SCROLL, MAX_SCROLL)
196206
if (amount > 0) {
197-
promises.push(mouse.scrollDown(amount).then(() => {}))
207+
promises.push(mouse.scrollDown(amount))
198208
} else if (amount < 0) {
199-
promises.push(mouse.scrollUp(-amount).then(() => {}))
209+
promises.push(mouse.scrollUp(-amount))
200210
}
201211
}
202212

203213
// Horizontal scroll
204214
if (this.isFiniteNumber(msg.dx) && Math.round(msg.dx) !== 0) {
205215
const amount = this.clamp(Math.round(msg.dx), -MAX_SCROLL, MAX_SCROLL)
206216
if (amount > 0) {
207-
promises.push(mouse.scrollRight(amount).then(() => {}))
217+
promises.push(mouse.scrollRight(amount))
208218
} else if (amount < 0) {
209-
promises.push(mouse.scrollLeft(-amount).then(() => {}))
219+
promises.push(mouse.scrollLeft(-amount))
210220
}
211221
}
212222

213223
if (promises.length) {
214-
await Promise.all(promises)
224+
const results = await Promise.allSettled(promises)
225+
for (const result of results) {
226+
if (result.status === "rejected") {
227+
console.error("Scroll event failed:", result.reason)
228+
}
229+
}
215230
}
216231
break
217232
}
@@ -247,17 +262,26 @@ export class InputHandler {
247262
console.log(`Processing key: ${msg.key}`)
248263
const nutKey = KEY_MAP[msg.key.toLowerCase()]
249264

250-
if (nutKey !== undefined) {
251-
await keyboard.pressKey(nutKey)
252-
await keyboard.releaseKey(nutKey)
253-
} else if (msg.key === " " || msg.key?.toLowerCase() === "space") {
254-
const spaceKey = KEY_MAP.space
255-
await keyboard.pressKey(spaceKey)
256-
await keyboard.releaseKey(spaceKey)
257-
} else if (msg.key.length === 1) {
258-
await keyboard.type(msg.key)
259-
} else {
260-
console.log(`Unmapped key: ${msg.key}`)
265+
try {
266+
if (nutKey !== undefined) {
267+
await keyboard.pressKey(nutKey)
268+
await keyboard.releaseKey(nutKey)
269+
} else if (msg.key === " " || msg.key?.toLowerCase() === "space") {
270+
const spaceKey = KEY_MAP.space
271+
await keyboard.pressKey(spaceKey)
272+
await keyboard.releaseKey(spaceKey)
273+
} else if (msg.key.length === 1) {
274+
await keyboard.type(msg.key)
275+
} else {
276+
console.log(`Unmapped key: ${msg.key}`)
277+
}
278+
} catch (err) {
279+
console.warn("Key press failed:", err)
280+
// ensure release just in case
281+
if (nutKey !== undefined)
282+
await keyboard.releaseKey(nutKey).catch(() => {})
283+
if (msg.key === " " || msg.key?.toLowerCase() === "space")
284+
await keyboard.releaseKey(KEY_MAP.space).catch(() => {})
261285
}
262286
}
263287
break
@@ -303,10 +327,13 @@ export class InputHandler {
303327
}
304328

305329
await new Promise((resolve) => setTimeout(resolve, 10))
330+
} catch (err) {
331+
console.error("Combo execution failed:", err)
306332
} finally {
307-
for (const k of pressedKeys.reverse()) {
308-
await keyboard.releaseKey(k)
309-
}
333+
const releasePromises = pressedKeys
334+
.reverse()
335+
.map((k) => keyboard.releaseKey(k))
336+
await Promise.allSettled(releasePromises)
310337
}
311338

312339
console.log(`Combo complete: ${msg.keys.join("+")}`)
@@ -315,7 +342,11 @@ export class InputHandler {
315342

316343
case "text":
317344
if (msg.text && typeof msg.text === "string") {
318-
await keyboard.type(msg.text)
345+
try {
346+
await keyboard.type(msg.text)
347+
} catch (err) {
348+
console.error("Failed to type text:", err)
349+
}
319350
}
320351
break
321352
}

src/server/getLocalIp.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import dgram from "node:dgram"
2+
3+
export async function getLocalIp(): Promise<string> {
4+
return new Promise((resolve) => {
5+
const socket = dgram.createSocket("udp4")
6+
let settled = false
7+
8+
const finish = (ip: string) => {
9+
if (settled) return
10+
settled = true
11+
12+
clearTimeout(timeout)
13+
socket.removeAllListeners("connect")
14+
socket.removeAllListeners("error")
15+
16+
try {
17+
socket.close()
18+
} catch {
19+
// socket may already be closed
20+
}
21+
22+
resolve(ip)
23+
}
24+
25+
const timeout = setTimeout(() => {
26+
finish("127.0.0.1")
27+
}, 1000)
28+
29+
socket.connect(1, "1.1.1.1")
30+
31+
socket.on("connect", () => {
32+
const addr = socket.address()
33+
if (typeof addr === "object") {
34+
finish(addr.address)
35+
} else {
36+
finish("127.0.0.1")
37+
}
38+
})
39+
40+
socket.on("error", () => {
41+
finish("127.0.0.1")
42+
})
43+
})
44+
}

src/server/websocket.ts

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
import fs from "node:fs"
22
import type { IncomingMessage } from "node:http"
33
import type { Socket } from "node:net"
4-
import os from "node:os"
54
import { WebSocket, WebSocketServer } from "ws"
65
import logger from "../utils/logger"
76
import { InputHandler, type InputMessage } from "./InputHandler"
8-
import type { Server as HttpServer } from "node:http"
9-
import type { Server as HttpsServer } from "node:https"
10-
11-
type CompatibleServer = HttpServer | HttpsServer
7+
import { getLocalIp } from "./getLocalIp"
128

139
import {
1410
generateToken,
@@ -18,18 +14,6 @@ import {
1814
touchToken,
1915
} from "./tokenStore"
2016

21-
function getLocalIp(): string {
22-
const nets = os.networkInterfaces()
23-
for (const name of Object.keys(nets)) {
24-
for (const net of nets[name] ?? []) {
25-
if (net.family === "IPv4" && !net.internal) {
26-
return net.address
27-
}
28-
}
29-
}
30-
return "localhost"
31-
}
32-
3317
function isLocalhost(request: IncomingMessage): boolean {
3418
const addr = request.socket.remoteAddress
3519
if (!addr) return false
@@ -41,8 +25,9 @@ interface ExtWebSocket extends WebSocket {
4125
isProvider?: boolean
4226
}
4327

44-
// server: any is used to support Vite's dynamic httpServer types (http, https, http2)
45-
export function createWsServer(server: CompatibleServer) {
28+
export async function createWsServer(
29+
server: NonNullable<import("vite").ViteDevServer["httpServer"]>,
30+
) {
4631
const configPath = "./src/server-config.json"
4732
let serverConfig: Record<string, unknown> = {}
4833
if (fs.existsSync(configPath)) {
@@ -63,7 +48,18 @@ export function createWsServer(server: CompatibleServer) {
6348

6449
const wss = new WebSocketServer({ noServer: true })
6550
const inputHandler = new InputHandler(inputThrottleMs)
66-
const LAN_IP = getLocalIp()
51+
let LAN_IP = "127.0.0.1"
52+
try {
53+
LAN_IP = await getLocalIp()
54+
} catch (error) {
55+
logger.warn(`Failed to resolve LAN IP, using localhost: ${String(error)}`)
56+
}
57+
58+
if (LAN_IP === "127.0.0.1") {
59+
logger.warn("LAN IP resolution fell back to localhost (127.0.0.1)")
60+
} else {
61+
logger.info(`Resolved LAN IP: ${LAN_IP}`)
62+
}
6763
const MAX_PAYLOAD_SIZE = 10 * 1024 // 10KB limit
6864

6965
logger.info("WebSocket server initialized")

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"jsx": "react-jsx",
66
"module": "ESNext",
77
"lib": ["ES2022", "DOM", "DOM.Iterable"],
8-
"types": ["vite/client"],
8+
"types": ["node","vite/client"],
99

1010
/* Bundler mode */
1111
"moduleResolution": "bundler",

0 commit comments

Comments
 (0)