Electron项目windows迁移信创后部分功能阻塞卡死问题解决过程备忘

码字不易,期待您的点赞

目录

项目背景

阅读说明

前情概要

简易架构图

问题描述

解决过程

思考1 事件循环冲突

第一步 尝试将所有第一步先把fsPromise 异步操作都换成了fs.writeFileSync类似的同步操作

第二步 尝试将所有文件处理放在主进程

第三步 通过work_thread实现

思考2 权限问题

思考3  第三方组件冲突

最终解决

结语


项目背景

        最近接手的一个新需求,需要将一款windows版本的electron桌面程序切换到信创环境下使用。项目中主要使用了electron中的BrowserWindow以及webview的功能。在webview中嵌入了全部业务逻辑的H5界面,界面使用vue3, 后端使用nodejs+express开发的。 全部功能是其它同事编写的,并且在windows环境下运行完全没有问题。

        因为了解到nodejs、以及electron中使用的chromium 都是支持跨平台的,并且兼容性都比较好。所以在开始刚处理这个需求的时候,认为简单熟悉一下项目代码,重新编译一下就能在linux环境下很好的运行起来。 但没有预测到的是,业务逻辑中有一个功能需要写入文件。每次执行到这块的逻辑就会导致整个程序阻塞、完全没有响应。同时系统CPU飙升,虚拟机是2核CPU。监控软件显示,其中electron进程能占用了120%左右。

阅读说明

        本篇文章重在记录问题的解决过程,尝试了很多方案。最终问题导致的原因仅供参考,不一定适用读者的情况。但是会给读者提供一些思路。 问题解决过程借助了AI(DeepSeek),确实能给解决提供很多思路。如果想直接看结论,可以滑到最后。  这里展示的项目架构仅为一个简易版本,为了说明这个问题的解决过程,读者无需纠结架构设计的合理性。

前情概要

简易架构图

        启动Electron进程之后,分为主进程和渲染进程 两个独立进程。 主进程中会先通过child_process.fork("./server/index.js") 启动web服务器,随后通过BrowserWindow.loadURL方法加载web页面。  

//主进程的main.js
function createWindow(){
    const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      devTools: true,
      sandbox:false,
      nodeIntegration: true, // 允许使用 Node.js API
      contextIsolation: false, // 禁用上下文隔离
      webSecurity: false, // 禁用 web 安全策略
      allowRunningInsecureContent:true,
      webviewTag: true, // 允许使用 webview 标签
    }
  })

    win.loadURL('http://localhost:13001');

}


app.whenReady().then(() => {
  let electron_server_process=child_process.fork(path.join(__dirname,'view-server','index.js'));
  createWindow();
  
})

        渲染进程会将preload.js加载到web页面中,在preload.js中会同步加载lib.js

        preload和lib的代码比较多,这里不列出来。后续说明问题时,逐一列出。

问题描述

        项目中有一个功能,在electron的web页面中,打开A目录的F文件,修改后再将文件内容更新到A目录F文件中。  F文件的读取和更新都是在lib.js中实现的,通过fs.writeFileSync 。

        问题:每次保存文件时,都出现了程序卡死,CPU飙升的情况。经过初步排查,就是在执行fs.wirteFileSync之后,如果把这个注释掉,程序不会出现问题。

解决过程

思考1 事件循环冲突

        因为在windows环境是正常的,所以怀疑是electron、chromium或node程序在linux环境有不一样的表现导致的。 AI分析给出了几种可能得原因,这里我比较倾向于是windows和linux环境的事件循环机制是不同的。于是按照这个思路修改代码解决。

        通过阅读代码,分析preload和lib程序中使用过fsPromise ,await、async 、fs.同步函数、http请求等与事件循环相关的代码。 因此怀疑是否是在linux环境下fsPromise或者其它异步函数的事件循环与web页面中的事件循环有冲突。

第一步 尝试将所有第一步先把fsPromise 异步操作都换成了fs.writeFileSync类似的同步操作

//渲染进程preload.js
//修改前
await fsPromise.writeFile(filename,content,"utf8");

//修改后
fs.writeFileSync(filename,content,"utf8");

        测试程序,从表现上看稍有缓解但未解决。

第二步 尝试将所有文件处理放在主进程

        第一步修改的内容都是在渲染进程中处理的,所以尝试将这些处理都放在主进程中。通过阅读Electron官方API文档,实现ipcRender.sendSync和ipcMain.on方法,实现渲染进程通知主进程调用fs.writeFileSync

// 主进程的main.js中 (whenReady事件函数内)

ipcMain.handle("writeFile",function(event,file,content){
	fs.writeFileSync(file,content);
	//event.returnValue=true;
return true;
})


//渲染进程的preload.js中  将所有的fs.writeFileSync改为如下
ipcRenderer.sendSync("writeFile",filepath,content);

问题仍未解决

第三步 通过work_thread实现

        分析第二步未解决的原因可能是sendSync这个函数可能会影响事件循环和回调,因此又通过AI获取到另外一个work_thread的方法,重新写代码验证,仍没有解决。还是会卡住。

// preload.js中新增函数

const { Worker,workerData } = require("node:worker_threads");

function thread_write_file(filepath, content,_callback){
	console.log("worker thread begin");
	 const worker = new Worker(path.join(__dirname,"thread_1.js"),{
		workerData:{
		    "file":filepath,
		    "content":content
		}
	 });
	 worker.on("message",function(data){
		console.log("on message:",data);
		if(data && data.result) {
			_callback && _callback(true)
		} else {
			_callback && _callback(false)
		}
	 })
	worker.on("close",function(){
		console.log("thread close");
	})
}

       

//新增thread_1.js
const { Worker,parentPort,workerData } = require("node:worker_threads");
const fs = require("node:fs");


console.log("workerData:",workerData);
try {
    fs.writeFileSync(workerData.file,workerData.content);
    parentPort.postMessage({
        result:true
    });
    console.log("writeFileSuccess")
} catch(e) {
    parentPort.postMessage({
        result:false,
        error: e.message,
        file: workerData.file
    });
    console.log("writeFile Failed:",e.message);
}

        这里尝试了两个版本的work_thread,如上是使用nodejs的word_thread,但Electron会报错,不允许使用这个库。

        下边使用了web api中的work线程,在MDN的Reference在线文档中可以找到使用说明。

//preload.js

function thread_write_file(filepath, content,_callback){
	console.log("worker thread begin");
	 const worker = new Worker("thread.js");
	 worker.addEventListener("message",function(event){
		console.log("on message:",event.data);
		if(event.data && event.data.result) {
			_callback && _callback(true)
		} else {
			_callback && _callback(false)
		}
	 })
worker.postMessage({
		    "file":filepath,
		    "content":content
		});
}
//thread.js

self.onmessage=function(workerData){
	try {
console.log("workerData:",workerData);
		fs.writeFileSync(workerData.file,workerData.content);
		self.postMessage({
		    result:true
		});
		console.log("writeFileSuccess")
	} catch(e) {
		self.postMessage({
		    result:false,
		    error: e.message,
		    file: workerData.file
		});
		console.log("writeFile Failed:",e.message);
	}
}

按照如上方法,将所有写文件的处理都通过执行thread_write_file代替,发现问题仍存在。

思考2 权限问题

        本小结无需提供代码

  1.    AI交流,分析可能与文件的读写权限有关系,所以又顺着这个方向查询验证,发现确实这个目录和文件是 drwxrwxr-x 权限,感觉基本上定位到了。但也有疑问,命名目录和文件都属于当前用户,为什么自己用户创建的文件,有w权限还不能写。难道是执行程序时用的other用户?
  2.     尝试使用chmod +777  增加权限, 验证了一次,发现好用了一下。但是过了一会又不好用了。继续分析会不会是目录和文件所属组、所有者和执行程序不一致导致的。发现没有这样的情况
  3.     尝试sudo执行electron,但electron启动之后一直白屏,且有终端报错信息,AI查询答复是sudo权限不能访问x- 桌面系统,之后放弃了这个方向
  4.     尝试将fs.WriteFileSync的操作重新放到主进程的child.fork子进程中实现,也就是在网页的13001服务端实现。不放在electron的进程了。重新写代码,验证问题仍未解决。
  5.     因为发现fs.ReadFileSync没有问题,只有write的是有问题,分析与文件写入操作会不会与其他地方有冲突。但不能确认具体位置 。

注意:这里发现:每次执行fs.writeFileSync之后,发现文件都是已经成功写入的。fs.writeFileSync后边的代码不能执行了。

思考3  第三方组件冲突

        经过了思考2的尝试,突然想到lib.js代码中有一段内容通过第三方组件chokidar实时监控文件变化,并重新读取处理。因此怀疑是这里冲突,将这段代码注释掉之后,发现确实没有问题了

//lib.js中的实施监控文件变更的关键代码
var chokidar = require('chokidar');
var watcher = chokidar.watch(actionsfilepath);
watcher.on('change', function(path) {
  console.log('File', path, 'has been changed');
  atomLogWrite.info("watcher.on:",'File['+ path+']has been changed');
  var tmp = path.split('\\');
  var i = 0;
  while(tmp[i] != "tmpfile") {
    i=i+1;
  }
  if((tmp.length-i)>4){
    if((tmp.length - i) == 6){
      var username = tmp[i+1];
      var proj = tmp[i+2];
      var branch = tmp[i+3];
      var groupname = tmp[i+4];
      tmp = tmp[i+5];
      tmp = tmp.split('\.');
      var ctrlname = tmp[0];
    }else if((tmp.length - i) == 5){
      var username = tmp[i+1];
      var proj = tmp[i+2];
      var branch = tmp[i+3];
      var groupname = "";
      tmp = tmp[i+4];
      tmp = tmp.split('\.');
      var ctrlname = tmp[0];
    }
    
    var scriptcontent = fs.readFileSync(path,'utf8');
    
     httpPost('/develop/state/savescript', { 'username': username, 'proj': proj, 'branch': branch, 'stateGroupName': groupname, 'name': ctrlname, 'script': scriptcontent }, (response) => {
      if (response.result == true) {
        console.log('save to server success');
        atomLogWrite.info("watcher.on:savescript:: save to server success");
      } else {
        console.log('save to server failed');
        atomLogWrite.info("watcher.on:savescript:: save to server failed");
      }
      });
    }
  }
});

        此时,分析应该是chokidar组件与fs.writeFileSync在linux环境中有冲突导致的,因此做了两种情况的尝试。

        第一分析是否是在watch的时候缺少某些参数,因此查询npm官网的chokidar包说明。分析可能与如下两个参数有关系

        所以尝试将其都配置为true  , 验证发现问题仍未解决。

//lib.js中变更的代码

var watcher = chokidar.watch(actionsfilepath ,{
    usePolling : true,
    interval  : 200 , //这里配200仅为尝试
    awaitWriteFinish :true
});

        检查了一下chokidar当前使用的版本和最新版本 ,发现都是4.0.3,应该使用的就是最新版本,无法通过升级解决。通过npm view chokidar version 查看npm服务器中的最新版本。

# 通过npm view chokidar version 查看npm服务器中的最新版本
# 将version 改为 versions 可以列举所有版本
star@star-vmwarevirtualplatform:~/code/app$ npm view chokidar version
4.0.3

   通过npm list chokidar version 查看本机安装的版本

star@star-vmwarevirtualplatform:~/code/app$ npm list chokidar version
electron-zjudp@1.0.0 /home/star/code/app
└── chokidar@4.0.3

       发现版本是最新的,不能通过升级解决。 所以做了第二次尝试去掉实时监控功能,由回调函数实现。

        此时的程序执行顺序是这样的:

  1.  web页面打开文件
  2.  lib.js中读取文件提供给web
  3. web中修改文件 并发送写入请求 给 web的server端 
  4.  sever端写入文件 结果响应给 web页面
  5.  web页面调用lib.js中的window.electron.onSave方法 
  6. onSave方法,执行原来chokidar监控的change事件里的方法

        如下提供了onsave方法的处理代码

//lib.js中去掉chokidar监控,将change事件的处理改为在onSave接口中调用

function fileChanged(filepath) {
setTimeout(()=>{
  console.log('lib::File', filepath, 'has been changed');
  atomLogWrite.info("watcher.on:",'File['+ filepath+']has been changed');
  var tmp = filepath.split("\\")//path.sep
  var i = 0;
  while(tmp[i] != "tmpfile") {
    i=i+1;
  }
  if((tmp.length-i)>4){
    if((tmp.length - i) == 6){
      var username = tmp[i+1];
      var proj = tmp[i+2];
      var branch = tmp[i+3];
      var groupname = tmp[i+4];
      tmp = tmp[i+5];
      tmp = tmp.split('\.');
      var ctrlname = tmp[0];
    }else if((tmp.length - i) == 5){
      var username = tmp[i+1];
      var proj = tmp[i+2];
      var branch = tmp[i+3];
      var groupname = "";
      tmp = tmp[i+4];
      tmp = tmp.split('\.');
      var ctrlname = tmp[0];
    }
    
    var scriptcontent = fs.readFileSync(filepath,'utf8');
    if(scriptcontent !=''){
      atomLogWrite.info("watcher.on:username = [%s],proj = [%s],branch = [%s],ctrlname = [%s],groupname[%s],path[%s]", username, proj, branch, ctrlname, groupname, filepath);
     
     httpPost('/develop/state/savescript', { 'username': username, 'proj': proj, 'branch': branch, 'stateGroupName': groupname, 'name': ctrlname, 'script': scriptcontent }, (response) => {
      if (response.result == true) {
        console.log('save to server success');
        atomLogWrite.info("watcher.on:savescript:: save to server success");
      } else {
        console.log('save to server failed');
        atomLogWrite.info("watcher.on:savescript:: save to server failed");
      }
      });
    }
  }
}, 500);
}

//window.electron={}是在preload.js中实现的
if(window.electron ){
	window.electron.onSave=fileChanged;
}

       这样操作之后仍然没有解决,还会卡住。 此时,再分析,可以排除是chokidar的原因。

最终解决

        经过多次尝试,都没有解决。现在发现能尝试的都已经试过了,只有httpPost异步请求这个方法没有验证过,分析可能还是与这里的异步处理有关系,可能会影响事件回调。

//lib.js


function getCookie(name) {
  // 1. 将 Cookie 字符串拆分为键值对数组
  const cookies = persist_cookie.split(';').map(cookie => cookie.trim());
  
  // 2. 查找目标 Cookie
  const targetCookie = cookies.find(cookie => cookie.startsWith(`${name}=`));
  
  // 3. 返回 Cookie 值(如果存在)
  return targetCookie ? targetCookie.split('=')[1] : null;
}
function httpPost(uri,data,cb){
  let postdata = JSON.stringify(data);
http.request({
  hostname: '127.0.0.1',
  port: 12001,
  path: uri,
  method: 'POST',
  headers: {
    'Content-Type': 'application/json;charset=UTF-8',
    'Content-Length': Buffer.byteLength(postdata),
    'accept': 'application/json, text/plain, */*',
    'Cookie': 'myapp='+getCookie('myapp'),
    'connection': 'keep-alive',
    'sec-fetch-dest':'empty',
    'sec-fetch-mode':'cors',
    'sec-fetch-site':'cross-site',
    'sec-fetch-storage-access':'active',
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) electron-zjudp/1.0.0 Chrome/136.0.7103.168 Electron/36.5.0 Safari/537.36'
  }
},(res) => {
  console.log(`STATUS: ${res.statusCode}`);
  //res.setEncoding('utf8');
  let responseData = '';
  res.on('data', (chunk) => {
    //console.log(`BODY: ${chunk}`);
    responseData+= chunk;
  });
  res.on('end', () => {
    cb(JSON.parse(responseData));
  });
  res.on('error', (err) => {
    console.error('Error:', err);
    cb({result:false, message: '请求失败'});
  })
  }
).end(postdata);
}

        因此决定在,fileChanged方法中,将httpPost的内容去掉。 但是在修改代码过程中无意间关注到了这段代码:

//lib.js的fileChanged函数中

 var tmp = filepath.split("\\")
  var i = 0;
  while(tmp[i] != "tmpfile") {
    i=i+1;
  }

        恍然大悟!!!! 

        这段代码,之前的开发人员这样写目的是想要通过循环的方式找到路径中tmpfile命名的目录。问题的原因就在这里, 因为现在是在linux环境下运行,所以filepath.split("\\")时,不应该使用\分割,在linux环境中应该是/ , 所以导致tmp数组本身只有一个元素,理论上while循环tmp[i] 永远不会是“tmpfile” , 这就产生的while死循环,cpu一直飙升。

        通过修改为如下代码验证, 完全解决了卡住问题,CPU也不会再飙升了,非常稳定:

//lib.js的fileChanged函数中


//将分隔符改为path.sep,可以兼容不同的操作系统
 var tmp = filepath.split(path.sep)
  var i = 0;
  while(tmp[i] != "tmpfile") {
    i=i+1;
  }

结语

        这个问题花费了1周的时间,每天白天有其它任务,晚上下班和周末分析、调试。终于找到了问题的直接原因。中途也考虑过不再分析,更换为其它的文件写入方案,但是处于找不到问题原因不甘心的心态,还是坚持到最后了。这一点应该非常应该表扬一下自己。

        查询这个问题耗时比较长的其中一个原因,和对代码的不熟悉的关系也非常大,接手别人的代码,粗略的读了一下整体执行逻辑就开始进程需求改造和调试了。 相信如果是代码的最初作者分析这个问题可能很快就能定位到。

        通过这次的代码分析,尝试和接触了很多东西,作为程序员真的是要多多的实战中、解决问题,这个过程比较艰难,但会学到知识、提高解决问题的能力和思路。

        这次解决问题的收获:

  • - 了解swap分区和功能
  • - 学习、编写到了node中的work_thread和浏览器中的Worker,两种工作线程
  • - 学习编写了Electron的 ipc通讯,使用了之前没有使用的方法。
  • - 把Electron的官网翻了一遍

    通过这个问题,还可以总结出来。可能有一些问题并不需要上升到太深层次的原理去分析,或者 不需要首先就去考虑是不是第三方程序的问题。 首先还是要把项目的源码内容读熟,理解,先从项目源码找问题。然后再排查三方组件、开源库等程序的问题,寻找解决方案。

        到这里,我的分享也结束了。

❤感谢阅读❤

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值