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

相关推荐
tiandyoin11 分钟前
调教 DeepSeek - 输出精致的 HTML MARKDOWN
前端·html
Electrolux2 小时前
【使用教程】一个前端写的自动化rpa工具
前端·javascript·程序员
赵大仁3 小时前
深入理解 Pinia:Vue 状态管理的革新与实践
前端·javascript·vue.js
小小小小宇3 小时前
业务项目中使用自定义Webpack 插件
前端
小小小小宇4 小时前
前端AST 节点类型
前端
小小小小宇4 小时前
业务项目中使用自定义eslint插件
前端
babicu1234 小时前
CSS Day07
java·前端·css
小小小小宇4 小时前
业务项目使用自定义babel插件
前端
前端码虫5 小时前
JS分支和循环
开发语言·前端·javascript
GISer_Jing5 小时前
MonitorSDK_性能监控(从Web Vital性能指标、PerformanceObserver API和具体代码实现)
开发语言·前端·javascript