2022.3.23日报:_open_osfhandle、_get_osfhandle不能跨模块调用(调试ASAR功能里发现的)

本文探讨了在Mini-Electron中遇到的asar文件加载错误,即UV_EBADF问题。问题根源在于_crt的_open_osfhandle函数在MT模式下导致的句柄不一致。Electron的asar机制通过注入asar_init.js来hook fs模块,当尝试读取asar内的文件时,会在C++层进行处理。解决方案涉及到理解并协调不同的模块和句柄管理。

1,今天在修改一个mini-electron的bug,就是mini-electron加载asar有时候提示文件UV_EBADF。

原因是asar的Archive::Archive函数,会调用_open_osfhandle获取fd,然后传到nodejs的

static void Read(const FunctionCallbackInfo<Value>& args)

里,再用_get_osfhandle获取fd对应的windows句柄。

然而这里有个问题,就是_open_osfhandle这些是crt的函数,如果我们编译的时候选择MT模式,也就是把crt编译到dll、exe里,就会有问题。因为_open_osfhandle内部是缓存了一个数组。这个数组在不同模块里肯定不是同一个。所以就导致了今天这个问题。在不同模块,调用_open_osfhandle、_get_osfhandle是不会成功的。

说到这里要讲解下electron的asar机制。

在apiasar.cpp里(原版electron应该也是类似的),initAsarSupport这个会注入一个asar_init.js文件,hook nodejs的fs模块。

当fs模块调用read 之类的函数,会走入asar_init.js里的hook函数。在这里面,会检测读写的路径是不是asar模块里的。如果是的话就走c++层读取真正的文件。

我原来有一个server。// server.js - 完整稳定版 | 适用于二轴摇杆 9 字节帧 (0xFF + X/Y/按钮 + 校验) console.log("🚀 正在启动自然算法展厅服务..."); // === 依赖检查 === let { SerialPort } = require(&#39;serialport&#39;); let WebSocket = require(&#39;ws&#39;); // === 配置参数(请根据实际情况修改)=== const SERIAL_PORT = &#39;COM5&#39;; // 🔧 修改为你自己的 COM 口(如 /dev/ttyUSB0 或 COM3) const BAUD_RATE = 9600; // 与 Arduino 一致 const WEBSOCKET_PORT = 8080; // 前端连接地址:ws://localhost:8080 // === 主程序 === async function startServer() { let port = null; let reconnectInterval = null; // 尝试打开串口 function openSerialPort() { if (reconnectInterval) return; // 防止重复设置 console.log(`📡 正在尝试连接串口 ${SERIAL_PORT} @ ${BAUD_RATE}bps...`); try { port = new SerialPort({ path: SERIAL_PORT, baudRate: BAUD_RATE, dataBits: 8, stopBits: 1, parity: &#39;none&#39; }); port.on(&#39;error&#39;, (err) => { console.error(&#39;🔴 串口错误:&#39;, err.message); closeAndReconnect(); }); port.on(&#39;close&#39;, () => { console.log(&#39;🔌 串口已关闭&#39;); closeAndReconnect(); }); port.once(&#39;open&#39;, () => { console.log(`🟢 成功打开串口 ${SERIAL_PORT}`); if (reconnectInterval) clearInterval(reconnectInterval); reconnectInterval = null; }); } catch (err) { console.error(&#39;❌ 打开串口失败:&#39;, err.message); closeAndReconnect(); } } // 断开后每 3 秒尝试重连 function closeAndReconnect() { if (reconnectInterval) return; console.log(&#39;⚠️ 串口断开,3秒后尝试重连...&#39;); reconnectInterval = setInterval(() => { openSerialPort(); }, 3000); } // 启动串口 openSerialPort(); // === WebSocket 服务器 === const wss = new WebSocket.Server({ port: WEBSOCKET_PORT }); wss.on(&#39;listening&#39;, () => { console.log(`🌐 WebSocket 服务器已启动: ws://localhost:${WEBSOCKET_PORT}`); }); wss.on(&#39;connection&#39;, (socket, req) => { const clientIP = req.socket.remoteAddress; console.log(`🎮 新客户端接入: ${clientIP}`); // 发送欢迎消息 socket.send(JSON.stringify({ type: &#39;welcome&#39;, time: Date.now(), msg: &#39;Connected to Joystick Server v1.0&#39; })); let bufferPool = Buffer.alloc(0); // 缓存未完成的数据块 // 处理串口数据流 const onData = (chunk) => { bufferPool = Buffer.concat([bufferPool, chunk]); // 循环查找以 0xFF 开头的有效帧 while (bufferPool.length >= 9) { const startIndex = bufferPool.indexOf(0xFF); if (startIndex === -1 || startIndex + 9 > bufferPool.length) break; const frame = bufferPool.slice(startIndex, startIndex + 9); bufferPool = bufferPool.slice(startIndex + 9); // 移除已处理部分 // 解析帧 const result = parseJoystickFrame(frame); if (!result) continue; // 校验失败则跳过 // 构造数据并广播给客户端 const data = { type: &#39;joystick&#39;, x: result.x, y: result.y, btn: result.btn, timestamp: Date.now() }; try { socket.send(JSON.stringify(data)); } catch (e) { // 忽略发送失败(客户端可能已断开) } } }; // 绑定监听 if (port && !port.listenerCount(&#39;data&#39;)) { port.on(&#39;data&#39;, onData); } // 客户端断开 socket.on(&#39;close&#39;, () => { console.log(`🔌 客户端断开: ${clientIP}`); port?.off(&#39;data&#39;, onData); // 移除该监听器 }); }); wss.on(&#39;error&#39;, (err) => { console.error(&#39;🔥 WebSocket 服务器启动失败:&#39;, err.message); process.exit(1); }); // Ctrl+C 优雅退出 process.on(&#39;SIGINT&#39;, () => { console.log(&#39;\n🛑 正在关闭服务...&#39;); port?.close(); wss.close(() => console.log(&#39;⏹️ WebSocket 已停止&#39;)); process.exit(0); }); } // === 解析 9 字节帧函数 === function parseJoystickFrame(buffer) { if (buffer.length !== 9) return null; if (buffer[0] !== 0xFF) return null; // 帧头必须是 0xFF // 计算校验和:前8字节之和 +1 对 256 取模 let sum = 0; for (let i = 0; i < 8; i++) sum += buffer[i]; const expectedChecksum = (sum + 1) % 256; const actualChecksum = buffer[8]; if (expectedChecksum !== actualChecksum) { console.warn(&#39;⚠️ 校验失败:&#39;, Array.from(buffer).map(b => b.toString(16).padStart(2, &#39;0&#39;)).join(&#39; &#39;), `| 校验: ${actualChecksum} ≠ ${expectedChecksum}` ); return null; } // 提取 X/Y 值(高位 << 8 | 低位) const xRaw = (buffer[1] << 8) | buffer[2]; // X 轴 ADC 值 const yRaw = (buffer[3] << 8) | buffer[4]; // Y 轴 ADC 值 const isPressed = Boolean(buffer[7] & 0x20); // 按钮状态:bit5 是否为 1 return { x: xRaw, y: yRaw, btn: isPressed }; } // === 启动入口 === startServer().catch(err => { console.error(&#39;🚨 程序异常终止:&#39;, err); });
最新发布
12-02
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值