Electron 桌面程序读取dll动态库

序幕:被GFW狙击的第一次构建

当我在工位上输入npm install electron时,控制台跳出的红色警报如同数字柏林墙上的一道弹痕:

复制代码
Error: connect ETIMEDOUT 104.20.22.46:443

网络问题不用愁,请移步我的另外文章进行配置:

electron 客户端 windows linux(麒麟V10)多系统离线打包 最新版 <一>_electron linux 离线打包-CSDN博客

第一章:构建electron-builder

builder排除文件夹,简单配置如下(package.json中):

复制代码
"build": {
    "appId": "com.example.win7app",
    "win": {
      "target": "nsis",
      "defaultArch": "ia32"
    },
    "extraFiles": [
      {
        "from": "resources",
        "to": "Resources",
        "filter": [
          "**/*"
        ]
      }
    ],
    "nsis": {
      "oneClick": false,
      "allowToChangeInstallationDirectory": true
    }
  },

我们需要在这个文件夹中读取dll文件,同时希望它打包后在安装目录下。

第二章:跨维度通信协议------主进程与渲染进程的量子纠缠

根目录添加preload.js,添加如下代码:

javascript 复制代码
// preload.js
const { contextBridge, ipcRenderer } = require('electron');

// 向渲染进程暴露安全的 API 方法
contextBridge.exposeInMainWorld(
  'electronAPI',
  {
    // 示例:调用 Node.js 文件系统 API
    readFile: async (path) => {
      const fs = await import('fs/promises');
      return fs.readFile(path, 'utf-8');
    },
    
    // 示例:进程间通信(IPC)
    openDialog: () => ipcRenderer.invoke('dialog:open'),

    // 检查是否存在加密狗并且是否匹配成功
    checkIfLock: () => ipcRenderer.invoke('checkIfLock'),
    captureUKey: () => ipcRenderer.invoke('captureUKey'),
    // 关闭窗口
    closeWindow: () => ipcRenderer.invoke('closeWindow'),

    // 监听打开设置
    onAction: (callback) => {
      ipcRenderer.on('renderer-action', (event, arg) => callback(arg))
    },
    
  }
)

然后再mainjs(electron主进程)中配置文件:

javascript 复制代码
let mainWindow, tray = null;
function createWindow() {
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'), // 指定预加载脚本
      contextIsolation: true, // 开启上下文隔离(安全必备)
      nodeIntegration: false  // 禁用直接 Node.js 访问
    }
  })

  // 隐藏菜单
  mainWindow.setMenu(null);

  // 加载本地页面(开发时可替换为本地服务地址,如 http://localhost:3000)
  mainWindow.loadFile(path.join(__dirname, 'src', 'index.html'));

  // 窗口关闭事件处理
  mainWindow.on('close', (event) => {
    if (!app.isQuiting) {
      event.preventDefault()
      const choice = dialog.showMessageBoxSync(mainWindow, {
        type: 'question',
        buttons: ['直接退出', '最小化到托盘'],
        title: '确认',
        message: '您要如何操作?',
        defaultId: 1
      })
      
      if (choice === 0) {
        app.isQuiting = true
        app.quit()
      } else {
        mainWindow.hide()
      }
    }
  })
}

最后在html中使用上述方法(html中使用):

javascript 复制代码
<script>
		
//【IPC通信】检测开关(true false)
const checkIfLock = window.electronAPI.checkIfLock;
const checkArm = window.electronAPI.captureUKey;
const closeWindow = window.electronAPI.closeWindow;
// 打开设置
window.electronAPI.onAction(({ type, data }) => {
  switch(type) {
    case 'openSettings':
      showSetting()
      break;
  }
})

// 显示设置
function showSetting () {
	try {
		var remortroot = localStorage.dpm_root;
		var remortport = localStorage.dpm_port;
		if (remortroot != null) {
			$("#remortroot").val(remortroot);
		}
		if (remortport != null) {
			$("#remortport").val(remortport);
		}
		$('#myModal').modal('show');
	} catch (err) {
		$('#myModal').modal('show');
	}
}

// ===========================================
var timeout;
var lockState = false;
$(function () {
	checkIfLock().then(res => {
		lockState = res;
	})
  //初始化设置
  var remortroot = localStorage.dpm_root;//服务器IP地址
  var remortport = localStorage.dpm_port;//端口号

  if (remortroot == null || remortroot == "" || remortroot == "undefined"
    || remortroot == null || remortroot == "" || remortroot == "undefined") {
    var err = "首次登录,请填写网络配置";
    showConfirmMsg(err, function (r) {
      if (r) {
        $('#myModal').modal('show');
      }
    });
  } else {
    loadLoginPage(remortroot, remortport);
  }
});

function isPort(str) {
  var parten = /^(\d)+$/g;
  if (parten.test(str) && parseInt(str) <= 65535 && parseInt(str) >= 0) {
    return true;
  } else {
    return false;
  }
}
function isIP(strIP) {
  var re = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/g
  if (re.test(strIP)) {
    if (RegExp.$1 < 256 && RegExp.$2 < 256 && RegExp.$3 < 256 && RegExp.$4 < 256) return true;
  }
  return false;
}
function sendUrlPort(remortroot, remortport) {
  var err = "";
  if (isIP(remortroot)) {
  } else {
    err += "服务器地址无效,";
  }
  if (isPort(remortport)) {
  } else {
    err += "端口号无效,";
  }
  if (err != "" && err.length > 0) {
    err = err.substring(0, err.length - 1);
    showConfirmMsg(err, function (r) {
      if (r) {
        $('#myModal').modal('show');
      }
    });
  } else {
    layer.msg('正在加载登录页面,请稍候。。。', {
      icon: 16,
      shade: 0.01,
      time: 5000000,
      shadeClose: false
    });
    var dpmHid = $("#eam_hid").text();
    var ifUd = "";
    if (false == lockState) {//false代表 u盾登录 需要验证uid
      ifUd = "xxx"
    }
    var url = "https://" + remortroot + ":" + remortport
    $.ajax({
      url: url,
      type: 'GET',
      timeout: 100000,
      datatype: "json",
      complete: function (response, textStatus) {
        //启动u盾检测
        timeout = setInterval("testUd()", 3000);

        layer.closeAll();
        if (response.status == 200) {
          localStorage.dpm_root = remortroot;
          localStorage.dpm_port = remortport;
          $('#github-iframe').attr('src', url);
        } else if (textStatus == 'timeout') {
          showConfirmMsg('未能成功连接系统(超时),请检查网络配置或联系管理员!', function (r) {
            if (r) {
              $('#myModal').modal('show');
            }
          });
        } else {
          showConfirmMsg('未能成功连接系统,请检查网络配置或联系管理员!', function (r) {
            if (r) {
              $('#myModal').modal('show');
            }
          });
        }
      }
    });
  }
}

//启动时:加载登录页,并判断u端是否存在
function loadLoginPage(remortroot, remortport) {
  checkArm().then(res => {
		if (true == lockState) {
			if (false == res.flag) {
				$("#eam_hid").text("");
			}
			sendUrlPort(remortroot, remortport);
		} else {
			if (true == res.flag) {
				$("#eam_hid").text(res.randomNum);
				sendUrlPort(remortroot, remortport);
			} else {
				showLongErrorMsg("未插入U盾");
			}
		}
	})
  
}
//启动后 监测u盾是否插入,未插入则退出系统
function testUd() {
	checkArm().then(res => {
		if (true == lockState) {
			if (false == res) {
				$("#eam_hid").text("");
			}
		} else {
			if (false == res) {
				clearInterval(timeout);
				showLongErrorMsg("未插入U盾");
			}
		}
	})
}
function showConfirmMsg(msg, callBack) {
  art.dialog({
    id: 'confirmId',
    title: '系统提示',
    content: msg,
    icon: 'warning',
    background: '#000000',
    opacity: 0.1,
    lock: true,
    button: [{
      name: '确定',
      callback: function () {
        callBack(true);
      },
      focus: true
    }]
  });
}
//错误提示
function showErrorMsg(msg) {
  top.art.dialog({
    id: 'errorId',
    title: '系统提示',
    content: msg,
    icon: 'error',
    time: 5,
    background: '#000',
    opacity: 0.1,
    lock: true,
    okVal: '关闭',
    ok: true
  });
}
function showLongErrorMsg(msg) {
  top.art.dialog({
    id: 'errorId',
    title: '5秒后自动关闭客户端...',
    content: msg,
    icon: 'error',
    time: 5,
    background: '#000',
    opacity: 0.1,
    lock: true,
    cancelVal: '关闭',
    cancel: function () {
      closeWindow();
    },
    close: function () {
      closeWindow();
    }
  });
}



//弹出框事件 
$("#initsetbtn").click(function () {
	var remortroot = $("#remortroot").val();
	var remortport = $("#remortport").val();
	$('#myModal').modal('hide');
	loadLoginPage(remortroot, remortport);
});
	</script>

第三章:调用dll

调用dll推荐使用koffi。另一篇文章也有说明:

Electron驯龙记:在Win7的废墟上唤醒32位DLL古老巨龙-CSDN博客

示例代码:

javascript 复制代码
// 在mainjs中

// 是否捕获U盾
ipcMain.handle('captureUKey', () => {
  return new Promise((resolve, reject) => {
   let promiseAry = [];
    var count = 0;
    while (count++ < 20) { //连续20次都失败 才认为失败
      promiseAry.push(checkOnceUKey());
    }
    Promise.all(promiseAry).then((results) => {
      console.log('所有检查结果:', results);
      if (Array.isArray(results) && results.length > 0) {
        resolve({
          flag: results.filter(item => item.flag == false).length == 0 ? true: false,
          randomNum: results[results.length - 1].randomNum || ''
        })
      } else {
        resolve({
          flag: false,
          randomNum: ''
        })
      }
      
    }) 
  }).catch(error => {
    console.error('是否捕获U盾出错:', error);
    return false; // 读取失败
  })
})
// 单次检查U盾
function checkOnceUKey() {
  return new Promise((resolve, reject) => {
    let flag = false;
    let randomNum = ''; // 随机数
    // 常量定义
    const DONGLE_SUCCESS = 0;
    const koffi = require('koffi');

    // 加载 DLL
    const dllPath = path.join(getDataPath(), 'Dongle_d.dll');
    const dongleLib = koffi.load(dllPath);

    // 定义结构体字段偏移量(单位:字节)
    const InfoStructOffsets = {
      m_Ver: 0,
      m_Type: 2,
      m_BirthDay: 4,
      m_Agent: 12,
      m_PID: 16,
      m_UserID: 20,
      m_HID: 24,
      m_IsMother: 32,
      m_DevType: 36
    };

    const InfoStructSize = 40;

    const Dongle_Enum = dongleLib.func('int Dongle_Enum(void*, int*)');
    const Dongle_Open = dongleLib.func('int Dongle_Open(int*, int)');
    const Dongle_ResetState = dongleLib.func('int Dongle_ResetState(int)');
    const Dongle_GenRandom = dongleLib.func('int Dongle_GenRandom(int, int, void*)');
    const Dongle_Close = dongleLib.func('int Dongle_Close(int)');

    // 初始化缓冲区
    const dongleInfo = Buffer.alloc(1024); // 假设最多 25 个设备(1024 / 40 ≈ 25)
    const countBuffer = Buffer.alloc(4);
    countBuffer.writeInt32LE(0, 0);

    // 1️⃣ 枚举设备
    let result = Dongle_Enum(dongleInfo, countBuffer);
    console.log(`** Dongle_Enum **: 0x${result.toString(16).padStart(8, '0')}`);

    if (result !== DONGLE_SUCCESS) {
      console.error(`** Enum errcode **: 0x${result.toString(16).padStart(8, '0')}`);
      flag = false;
    }

    const deviceCount = countBuffer.readInt32LE(0);
    console.log(`** Find Device **:  ${deviceCount}`);

    if (deviceCount === 0) {
      console.log('** No Device **');
      flag = false;
    }

    // 3️⃣ 打开设备
    const handleBuffer = Buffer.alloc(4);
    result = Dongle_Open(handleBuffer, 0);
    const handle = handleBuffer.readInt32LE(0);
    console.log(`** Dongle_Open **: 0x${result.toString(16).padStart(8, '0')}`);
    if (result !== DONGLE_SUCCESS) {
      console.error(`** Open Failed **`);
      flag = false;
    } else {
      console.log(`** Open Success **: [handle=0x${handle.toString(16).padStart(8, '0')}]`);
      randomNum = `0x${handle.toString(16).padStart(8, '0')}`;
      Dongle_Close(handle);
      flag = true;
    }

    // 4️⃣ 重置 COS 状态
    /*
    result = Dongle_ResetState(handle);
    console.log(`Dongle_ResetState 返回值: 0x${result.toString(16).padStart(8, '0')}`);
    if (result !== DONGLE_SUCCESS) {
      console.error(`重置 COS 状态失败`);
      Dongle_Close(handle);
      return;
    }
    console.log('重置 COS 状态成功');
    */
  
    // 5️⃣ 生成随机数
    // const randomLen = 16;
    // const randomBuffer = Buffer.alloc(randomLen);
    // result = Dongle_GenRandom(handle, randomLen, randomBuffer);
    // console.log(`Dongle_GenRandom : 0x${result.toString(16).padStart(8, '0')}`);
    // if (result !== DONGLE_SUCCESS) {
    //   console.error(`生成随机数失败`);
    //   Dongle_Close(handle);
    // } else {
    //   randomNum = randomBuffer.toJSON().data.map(b => PrefixZero(b, 2)).join(' ').toUpperCase();
    //   Dongle_Close(handle);
    // }
    //console.log(`随机数据: ${randomBuffer.toJSON().data.map(b => PrefixZero(b, 2)).join(' ').toUpperCase()}`);
    /*
    // 6️⃣ 关闭设备
    result = Dongle_Close(handle);
    console.log(`Dongle_Close 返回值: 0x${result.toString(16).padStart(8, '0')}`);
    if (result !== DONGLE_SUCCESS) {
      console.error(`关闭设备失败`);
      return;
    }
    console.log('成功关闭设备');
    */
   resolve({
    flag,
    randomNum
   })
  }).catch(err => {
    console.error('单次读取U盾失败:', err);
    return false; // 读取失败
  })
}

后记:与时间赛跑的混乱代码之旅

回首这次Electron的改造征程,更像是一场与编译警告共舞的午夜狂奔。由于项目周期紧张,某些技术方案难免带着「先跑起来再优化」的仓促痕迹------就像在暴雨中搭建帐篷,难免会有几处漏水的接缝。

过程中那些临时添加的Webpack补丁、为绕过环境问题硬编码的路径、甚至为了紧急交付保留的TODO注释,都如同代码迷宫中未清理的记号。虽然最终功能得以实现,但我深知这座代码大厦的某些承重墙上,或许还留着需要加固的裂缝。

在此特别恳请各位同行:若您在阅读中发现任何逻辑漏洞、安全隐患或架构缺陷,请务必通过Issue或邮件指正。您的一条建议,或许就能避免某个深夜的生产环境告警。技术之路本就如履薄冰,唯有开放交流才能让我们的每一步走得更稳。

~ end

相关推荐
Lupino23 分钟前
被 React “玩弄”的 24 小时:为了修一个不存在的 Bug,我给大模型送了顿火锅钱
前端·react.js
米丘29 分钟前
了解 Javascript 模块化,更好地掌握 Vite 、Webpack、Rollup 等打包工具
前端
Heo31 分钟前
深入 React19 Diff 算法
前端·javascript·面试
滕青山32 分钟前
个人所得税计算器 在线工具核心JS实现
前端·javascript·vue.js
小怪点点33 分钟前
手写promise
前端·promise
国思RDIF框架42 分钟前
RDIFramework.NET Web 敏捷开发框架 V6.3 发布 (.NET8+、Framework 双引擎)
前端
颜酱43 分钟前
从0到1实现LFU缓存:思路拆解+代码落地
javascript·后端·算法
Mintopia43 分钟前
如何在有限的时间里,活出几倍的人生
前端
炫饭第一名43 分钟前
速通Canvas指北🦮——变形、渐变与阴影篇
前端·javascript·程序员
Neptune144 分钟前
让我带你迅速吃透React组件通信:从入门到精通(上篇)
前端·javascript