fix(gui): 启动期误判服务端未启动;keybd_event 类型名致组合键全废

## 修 GUI 反复误判「服务端未启动」
isServerRunning() 硬编码 http://localhost:8080/。实测该部署下服务端在
192.168.2.60:8080(homed 跑在 WSL 里):
  localhost:8080      -> 000 连不上
  192.168.2.60:8080   -> 302 正常

于是每次启动都:探测失败 → 误判服务没起 → 去 startHomed() 拉起 GUI 旁
并不存在的 homed.exe(findHomed 返回 null 就 skip)→ 再 waitForServer()
干等 8s 超时 → 打印 "homed failed to start within timeout"。
gui.log 里这条 ERR 累计出现 53 次,是日志里唯一的错误项。

改法:
  · serverProbeTargets() 按「connections.json 里已配置的连接(当前连接
    优先)→ localhost」顺序探测,命中任一即视为在线
  · 服务可达时直接 skip homed 自动拉起
  · 无本地 homed 可执行文件时不再空等超时,直接说明情况
  · probeOne 加超时保护,避免启动被挂住

实测:修复后启动日志为 "server reachable, skip homed autostart",
该 ERR 不再出现。

## 修 keybd_event 类型名(组合键整体失效)
koffi 不认 Win32 的 `byte`:
  "void keybd_event(byte bVk, ...)"
    -> Error: Unknown or invalid type name 'byte'
  "void keybd_event(uint8 bVk, ...)"  -> 正常

原代码用的正是 `byte`,且整段包在空 catch 里,于是 keybd_event 恒为 null,
所有组合键(keypress/hotkey)都返回 "keybd_event unavailable"。
这个 bug 很难被发现:单键 click/move/scroll 走 mouse_event,不受影响。

端到端实测确认:修复前 hotkey ctrl+shift+t 报
  computeruse hotkey: keybd_event unavailable
另外签名解析失败现在会打日志,不再静默。

## 判据
computeruse.test.mjs 扩到 38 条(进 npm test,全量 121 条全绿),新增:
  · keybd_event 签名类型(且已做变异验证:改回 byte 判据立即转红)
  · 服务探测不只探 localhost / localhost 仅兜底 / 仅当有 homed 才拉起
  · 服务可达时 skip autostart / 探测有超时保护
This commit is contained in:
JianFeeeee
2026-10-02 13:03:17 +08:00
parent 40119e8116
commit d6dd84e1c8
2 changed files with 169 additions and 65 deletions

View File

@ -297,29 +297,83 @@ function findHomed() {
return fs.existsSync(p) ? p : null;
}
function isServerRunning() {
// ★ 探测服务端候选地址。
//
// 旧实现硬编码 http://localhost:8080/,在「服务端跑在 WSL/另一台机器」
// 的常见部署下必然探不通,进而误判服务未启动、去拉起 GUI 旁边并不存在的
// homed.exe,最后打印 "homed failed to start within timeout"。
// 实测证据:localhost:8080 返回 000,而 connections.json 里配置的
// 192.168.2.60:8080 返回 302 —— 服务一直好好地在那儿。
//
// 现在按「已配置的连接 → localhost」顺序探,命中任一即视为在线。
function serverProbeTargets() {
const targets = [];
const seen = new Set();
const push = (u) => {
if (!u) return;
const s = String(u).replace(/\/+$/, "");
if (!/^https?:\/\//.test(s)) return;
if (seen.has(s)) return;
seen.add(s);
targets.push(s);
};
try {
const data = loadConnections();
// loadConnections 返回 {connections:[...], currentId},
// 不是数组(早期按数组写的判断在此处不成立)。
const list = data && Array.isArray(data.connections) ? data.connections : [];
// 当前连接优先,其余也一并探测(用户可能切过连接)
const curId = data ? data.currentId : null;
const ordered = list
.slice()
.sort((a, b) => (a && b && a.id === curId ? -1 : 0));
for (const c of ordered) if (c && c.url) push(c.url);
} catch (e) {}
push("http://localhost:8080");
return targets;
}
function probeOne(target, timeoutMs) {
return new Promise((resolve) => {
const req = http.get("http://localhost:8080/", () => resolve(true));
req.on("error", () => resolve(false));
req.setTimeout(2000, () => {
req.destroy();
resolve(false);
let done = false;
const finish = (ok, via) => {
if (done) return;
done = true;
resolve({ ok, via });
};
let req;
try {
req = http.get(target + "/", () => finish(true, target));
} catch (e) {
return finish(false, target);
}
req.on("error", () => finish(false, target));
req.setTimeout(timeoutMs || 2000, () => {
try {
req.destroy();
} catch (e) {}
finish(false, target);
});
});
}
function waitForServer(maxWait = 8000) {
return new Promise((resolve) => {
const start = Date.now();
const check = () => {
isServerRunning().then((running) => {
if (running) return resolve(true);
if (Date.now() - start > maxWait) return resolve(false);
setTimeout(check, 300);
});
};
check();
});
// 任一候选端点可达即认为服务在线。
async function isServerRunning() {
const targets = serverProbeTargets();
for (const t of targets) {
const r = await probeOne(t, 1500);
if (r.ok) return true;
}
return false;
}
async function waitForServer(maxWait = 8000) {
const start = Date.now();
for (;;) {
if (await isServerRunning()) return true;
if (Date.now() - start > maxWait) return false;
await new Promise((r) => setTimeout(r, 300));
}
}
function startHomed() {
@ -1965,8 +2019,20 @@ function executeHomeagentCmd(capability, reqId) {
const VK_EXTENDED = new Set([0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,0x2e,0x5b,0x5c,0x6f,0x74]);
// keybd_event 全局声明(拿得到 user32 后用)
//
// ★ 类型名必须是 uint8:koffi 不认 Win32 头文件里的 `byte`,
// 实测 `void keybd_event(byte bVk, ...)` 抛
// "Unknown or invalid type name 'byte'",而 uint8 可正常解析。
// 这个 bug 曾让组合键(hotkey/keypress)整体报
// "keybd_event unavailable" —— 单键 click 不受影响,所以很容易漏掉。
let keybd_event = null;
try { keybd_event = user32.func("void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo)"); } catch (e) {}
try {
keybd_event = user32.func(
"void keybd_event(uint8 bVk, uint8 bScan, uint32 dwFlags, uintptr_t dwExtraInfo)",
);
} catch (e) {
console.error("keybd_event signature failed: " + e.message);
}
// tapVk 按键一次:支持 "ctrl+c"、"ctrl+shift+t"、"alt+f4"
const tapVk = (spec) => {
@ -2784,7 +2850,16 @@ function destroyTray() {
app.whenReady().then(async () => {
installAuthRule();
const running = await isServerRunning();
if (!running) {
if (running) {
// 已探测到可用端点:服务端在别处正常运行(远程/WSL/另一台机器)。
// 这类部署下 GUI 不该、也拉不起 homed —— 此前仍会去 startHomed()
// 再等 8s 超时,刷出误导性的 "homed failed to start within timeout"。
console.log("server reachable, skip homed autostart");
} else if (!findHomed()) {
// 没有本地 homed 可执行文件 ⇒ 本机根本不具备托管服务端的能力,
// 此时干等超时毫无意义,直接说明情况即可。
console.log("no local homed binary and no reachable endpoint; GUI will use configured connection");
} else {
startHomed();
const started = await waitForServer();
if (started) {