main.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. const { app, BrowserWindow, Tray, Menu } = require('electron');
  2. const path = require('path');
  3. const serverManager = require('./serverManager');
  4. const menuManager = require('./menuManager');
  5. const updateManager = require('./updateManager');
  6. const constants = require('./constants');
  7. const { getNetworkInterfaces } = require('./networkService');
  8. const { exec } = require('child_process');
  9. const net = require('net');
  10. const os = require('os');
  11. const utils = require('./utils');
  12. const fs = require('fs');
  13. let win = null;
  14. let tray = null;
  15. app.isQuitting = false; // 添加标志位
  16. // 根据操作系统选择命令
  17. const IS_WINDOWS = os.platform() === 'win32';
  18. const FIND_PORT_COMMAND = IS_WINDOWS ? `netstat -ano | findstr :${constants.port}` : `lsof -i :${constants.port} -t`;
  19. const KILL_PROCESS_COMMAND = IS_WINDOWS ? `taskkill /PID {PID} /F` : `kill -9 {PID}`;
  20. // 创建 Electron 窗口的函数
  21. const createWindow = async () => {
  22. try {
  23. win = new BrowserWindow({
  24. width: 800,
  25. height: 600,
  26. webPreferences: {
  27. nodeIntegration: true,
  28. contextIsolation: false,
  29. },
  30. });
  31. //win.webContents.openDevTools(); // 打开开发者工具
  32. //win.webContents.session.setCertificateVerifyProc((request, callback) => {
  33. // // 始终返回 0 表示验证通过
  34. // callback(0);
  35. //});///login/admin
  36. win.maximize();
  37. if (app.isPackaged) {
  38. let indexPath = path.join(constants.serverPath, 'resources', 'index.html');
  39. win.loadFile(indexPath);
  40. } else {
  41. win.loadFile('index.html');
  42. }
  43. // 模拟执行业务过程
  44. StartProcess();
  45. //win.loadURL(`${constants.baseUrl}/login/admin`, {
  46. // agent: constants.agent
  47. //});
  48. // 监听窗口关闭事件,隐藏窗口而不是关闭
  49. win.on('close', (event) => {
  50. if (!app.isQuitting) {
  51. event.preventDefault();
  52. win.hide();
  53. }
  54. });
  55. } catch (error) {
  56. console.error('Error starting server or loading window:', error);
  57. }
  58. };
  59. // 当 Electron 应用准备好时创建窗口
  60. app.whenReady().then(() => {
  61. process.env.NODE_OPTIONS = '--tls-min-v1.2';
  62. createWindow();
  63. createTray();
  64. // 创建菜单并传递回调函数
  65. menuManager.createMenu(checkForUpdatesHandler);
  66. app.on('activate', () => {
  67. if (BrowserWindow.getAllWindows().length === 0) {
  68. createWindow();
  69. }
  70. });
  71. // 监听 before-quit 事件,关闭 IES.ExamServer.exe 进程
  72. app.on('before-quit', async (event) => {
  73. if (app.isQuitting) {
  74. return; // 如果已经在退出流程中,则直接返回
  75. }
  76. app.isQuitting = true; // 标记正在退出
  77. event.preventDefault(); // 阻止默认的退出行为
  78. await serverManager.shutdownServer(); // 关闭服务器
  79. app.quit(); // 触发退出流程
  80. });
  81. });
  82. // 当所有窗口关闭时退出应用(macOS 除外)
  83. app.on('window-all-closed', function () {
  84. if (process.platform !== 'darwin') {
  85. app.quit();
  86. }
  87. });
  88. // 服务端启动过程
  89. async function StartProcess() {
  90. //步骤1 开始检查
  91. sendLogMessage('检查评测服务是否启动...');
  92. //步骤2 检查端口是否占用。
  93. let serverStarted = false;
  94. try {
  95. const inUse = await isPortInUse(constants.port);
  96. if (inUse) {
  97. serverStarted = true;
  98. sendLogMessage(`检查到端口被占用...`);
  99. } else {
  100. sendLogMessage(`评测服务未启动...`);
  101. }
  102. } catch (err) {
  103. sendLogMessage(`检查端口时出错...`);
  104. // console.log(`检查评测服务状态出错...`, err);
  105. }
  106. //步骤3,尝试优雅关闭,如果不能关闭再通过 关闭进程号关闭端口
  107. let needStart = false;
  108. if (serverStarted) {
  109. //检测是否是.net6的服务在线
  110. sendLogMessage('检查是否是评测服务占用端口...');
  111. const isServerRunning = await serverManager.checkServerHealth();
  112. if (!isServerRunning) {
  113. //可能是其他程序占用
  114. sendLogMessage('检测到其他程序占用端口...');
  115. sendLogMessage('正在查找占用端口的进程...');
  116. const pid = await findProcessUsingPort();
  117. sendLogMessage(`找到占用端口的进程: ${pid}`);
  118. sendLogMessage('正在关闭进程...');
  119. const killResult = await killProcess(pid);
  120. sendLogMessage(`进程关闭结果:${killResult}`);
  121. utils.delay(500);
  122. needStart = true;
  123. } else {
  124. sendLogMessage('评测服务正在运行中...');
  125. sendLogMessage('如需强制退出,请点击<设置><退出>按钮...');
  126. }
  127. } else {
  128. needStart = true;
  129. }
  130. if (needStart) {
  131. sendLogMessage('正在启动评测服务...');
  132. await serverManager.startServer();
  133. const isServerRunning = await serverManager.checkServerHealth();
  134. if (isServerRunning) {
  135. let startJsonPath = path.join(constants.serverPath, 'server',"wwwroot", 'start.json');
  136. //startJsonPath = `"${startJsonPath}"`;
  137. // sendLogMessage(`"${startJsonPath}"`);
  138. let jump = true;
  139. sendLogMessage('确认评测服务启动状态...');
  140. if (fs.existsSync(startJsonPath)) {
  141. sendLogMessage('检测IP域名映射配置...');
  142. sendLogMessage('检测SSL安全证书...');
  143. // 读取JSON文件
  144. const data = fs.readFileSync(`${startJsonPath}`, 'utf8');
  145. const jsonData = JSON.parse(data);
  146. if (jsonData.read === 0) {
  147. // 检查服务启动是否正常
  148. if (jsonData.code_zip === 200 && jsonData.code_cer === 200 && jsonData.code_hosts === 200) {
  149. sendLogMessage('服务启动正常...');
  150. } else {
  151. jump = false;
  152. sendLogMessage('服务启动异常,请检查相关配置。' + JSON.stringify(jsonData) );
  153. }
  154. // IP域名映射是否成功
  155. sendLogMessage(`IP域名映射状态:${jsonData.hosts_msg}`);
  156. // 安全证书是否安装成功
  157. sendLogMessage(`安全证书状态:${jsonData.cer_msg}`);
  158. // 使用的网卡名称及mac
  159. sendLogMessage(`网卡名称:${jsonData.name}`);
  160. sendLogMessage(`MAC地址:${jsonData.mac}`);
  161. // 映射的IP地址是多少
  162. sendLogMessage(`IP地址:${jsonData.ip}`);
  163. // 安全证书是否安装成功
  164. sendLogMessage(`学生的配置文件:${jsonData.zip_msg}`);
  165. // 将read状态改为1,标记已读
  166. jsonData.read = 1;
  167. // 写入start.json
  168. fs.writeFileSync(startJsonPath, JSON.stringify(jsonData, null, 2));
  169. //sendLogMessage('已标记为已读并更新JSON文件。');
  170. } else {
  171. // sendLogMessage('该JSON文件已读,无需再次检查。');
  172. }
  173. } else {
  174. jump = false;
  175. sendLogMessage(`指定的JSON文件不存在。${startJsonPath}`);
  176. }
  177. if (jump) {
  178. sendLogMessage('评测服务启动成功...');
  179. sendLogMessage('正在加载登录页面...');
  180. // 使用 setTimeout 延迟 5 秒后执行 win.loadURL
  181. setTimeout(() => {
  182. win.loadURL(`${constants.baseUrl}/login/admin`, {
  183. agent: constants.agent
  184. });
  185. }, 3000);
  186. }
  187. }
  188. else {
  189. sendLogMessage('评测服务启动失败...');
  190. sendLogMessage('请检查hosts文件是否自动映射了exam.habook.local域名...');
  191. sendLogMessage("如果没有请手动执行<teacher_manual.bat>脚本文件...");
  192. }
  193. } else
  194. {
  195. sendLogMessage('正在加载登录页面...');
  196. utils.delay(2000)
  197. win.loadURL(`${constants.baseUrl}/login/admin`, {
  198. agent: constants.agent
  199. });
  200. }
  201. }
  202. // 发送消息到渲染进程
  203. function sendLogMessage(message) {
  204. if (win) {
  205. win.webContents.send('log-message', message);
  206. }
  207. }
  208. const createTray = () => {
  209. const iconPath = path.join(__dirname, 'logo.ico'); // 你的托盘图标路径
  210. tray = new Tray(iconPath);
  211. const contextMenu = Menu.buildFromTemplate([
  212. {
  213. label: '显示',
  214. click: () => {
  215. if (win) {
  216. win.show();
  217. }
  218. }
  219. },
  220. {
  221. label: '退出',
  222. click: () => {
  223. require('electron').app.quit();
  224. //app.isQuitting = true;
  225. //app.quit();
  226. }
  227. }
  228. ]);
  229. tray.setToolTip('评测教师端');
  230. tray.setContextMenu(contextMenu);
  231. // 监听双击托盘图标事件,恢复窗口
  232. tray.on('double-click', () => {
  233. if (win) {
  234. if (win.isVisible()) {
  235. win.focus(); // 如果窗口已经显示,则聚焦窗口
  236. } else {
  237. win.show(); // 如果窗口隐藏,则显示窗口
  238. }
  239. }
  240. });
  241. };
  242. // 定义回调函数
  243. const checkForUpdatesHandler = () => {
  244. updateManager.checkForUpdates(win, () => {
  245. menuManager.createMenu(checkForUpdatesHandler); // 重新创建菜单并传递回调函数
  246. });
  247. };
  248. function isPortInUse(port) {
  249. return new Promise((resolve, reject) => {
  250. const server = net.createServer()
  251. .once('error', (err) => {
  252. if (err.code === 'EADDRINUSE') {
  253. resolve(true); // 端口被占用
  254. } else {
  255. reject(err);
  256. }
  257. })
  258. .once('listening', () => {
  259. server.close();
  260. resolve(false); // 端口未被占用
  261. })
  262. .listen(port);
  263. });
  264. }
  265. // 查找占用端口的进程
  266. function findProcessUsingPort() {
  267. return new Promise((resolve, reject) => {
  268. exec(FIND_PORT_COMMAND, (error, stdout, stderr) => {
  269. if (error) {
  270. if (error.code === 1) {
  271. // 未找到占用端口的进程
  272. resolve(null);
  273. } else {
  274. reject(`查找端口占用失败: ${stderr}`);
  275. }
  276. return;
  277. }
  278. // 提取 PID
  279. const pid = IS_WINDOWS
  280. ? stdout.trim().split(/\s+/).pop() // Windows: 取最后一列
  281. : stdout.trim(); // macOS/Linux: 直接输出 PID
  282. resolve(pid);
  283. });
  284. });
  285. }
  286. // 关闭进程
  287. function killProcess(pid) {
  288. return new Promise((resolve, reject) => {
  289. if (!pid) {
  290. resolve('未找到占用端口的进程');
  291. return;
  292. }
  293. const command = KILL_PROCESS_COMMAND.replace('{PID}', pid);
  294. exec(command, (error, stdout, stderr) => {
  295. if (error) {
  296. reject(`关闭进程失败: ${stderr}`);
  297. return;
  298. }
  299. resolve(`已关闭进程: ${pid}`);
  300. });
  301. });
  302. }
  303. //// 启动 .NET Core 应用程序
  304. //function startDotNetApp() {
  305. // return new Promise((resolve, reject) => {
  306. // const command = `dotnet run --urls=http://localhost:${PORT}`;
  307. // const options = { cwd: DOTNET_PROJECT_PATH }; // 设置工作目录为 .NET 项目路径
  308. // exec(command, options, (error, stdout, stderr) => {
  309. // if (error) {
  310. // reject(`启动 .NET Core 应用程序失败: ${stderr}`);
  311. // return;
  312. // }
  313. // resolve(`.NET Core 应用程序已启动: ${stdout}`);
  314. // });
  315. // });
  316. //}