Eden 2 月之前
父節點
當前提交
95835db2ca

+ 165 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/app.js

@@ -0,0 +1,165 @@
+
+const { app, BrowserWindow, Tray, Menu } = require('electron');
+const path = require('path');
+const serverManager = require('./serverManager');
+const menuManager = require('./menuManager');
+const updateManager = require('./updateManager');
+const constants = require('./constants');
+const { getNetworkInterfaces } = require('./networkService');
+const { exec } = require('child_process');
+ 
+//app.disableHardwareAcceleration(); //使用windows7 ,或者虚拟机的时候 需要验证 禁用 GPU 加速
+//app.commandLine.appendSwitch('ignore-certificate-errors')
+let win = null;
+let tray = null;
+app.isQuitting = false; // 添加标志位
+// 创建 Electron 窗口的函数
+const createWindow = async () => {
+    try {
+        const networks = getNetworkInterfaces();
+      //  console.log('Available networks:', networks);
+        console.log("运行地址:", networks[0].ip)
+        try {
+            let filePath = path.join(constants.serverPath,'certificate.bat');
+            filePath = `"${filePath}"`;
+            // 使用 execFile 执行 .bat 文件
+     
+            // 执行 .bat 文件
+
+            // 设置超时时间为 60 秒
+            exec(filePath,  (error, stdout, stderr) => {
+                if (error) {
+                    if (error.code === 'ETIMEDOUT') {
+                        console.error('执行 .bat 文件超时');
+                    } else {
+                        console.error(`执行 .bat 文件时出错: ${error.message}`);
+                    }
+                    return;
+                }
+                if (stderr) {
+                    console.error(`.bat 文件执行过程中出现错误: ${stderr}`);
+                    return;
+                }
+                console.log(`.bat 文件执行结果: ${stdout}`);
+            });
+        } catch (error)
+        {
+            console.log(`脚本启动执行错误: ${error}`);
+        }
+
+
+        console.log("开始检查是否启动服务...")
+        const isServerRunning = await serverManager.checkServerHealth();
+        if (!isServerRunning) {
+            console.log('Server is not running, starting it...');
+            await serverManager.startServer(); // 启动 Web API
+        }
+        win = new BrowserWindow({
+            width: 800,
+            height: 600,
+            webPreferences: {
+                nodeIntegration: true,
+                contextIsolation: false,
+            },
+        });
+        //win.webContents.session.setCertificateVerifyProc((request, callback) => {
+        //    // 始终返回 0 表示验证通过
+        //    callback(0);
+        //});///login/admin
+        win.maximize();
+        win.loadURL(`${constants.baseUrl}/login/admin`, {
+            agent: constants.agent
+        });
+        // 监听窗口关闭事件,隐藏窗口而不是关闭
+        win.on('close', (event) => {
+            if (!app.isQuitting) {
+                event.preventDefault();
+                win.hide();
+            }
+        });
+
+    } catch (error) {
+        console.error('Error starting server or loading window:', error);
+    }
+};
+
+
+const createTray = () => {
+    const iconPath = path.join(__dirname, 'logo.ico'); // 你的托盘图标路径
+    tray = new Tray(iconPath);
+
+    const contextMenu = Menu.buildFromTemplate([
+        {
+            label: '显示',
+            click: () => {
+                if (win) {
+                    win.show();
+                }
+            }
+        },
+        {
+            label: '退出',
+            click: () => {
+                require('electron').app.quit();
+                //app.isQuitting = true;
+                //app.quit();
+            }
+        }
+    ]);
+
+    tray.setToolTip('评测教师端');
+    tray.setContextMenu(contextMenu);
+
+    // 监听双击托盘图标事件,恢复窗口
+    tray.on('double-click', () => {
+        if (win) {
+            if (win.isVisible()) {
+                win.focus(); // 如果窗口已经显示,则聚焦窗口
+            } else {
+                win.show(); // 如果窗口隐藏,则显示窗口
+            }
+        }
+    });
+};
+
+
+
+
+// 定义回调函数
+const checkForUpdatesHandler = () => {
+    updateManager.checkForUpdates(win, () => {
+        menuManager.createMenu(checkForUpdatesHandler); // 重新创建菜单并传递回调函数
+    });
+};
+// 当 Electron 应用准备好时创建窗口
+app.whenReady().then(() => {
+    process.env.NODE_OPTIONS = '--tls-min-v1.2';
+    createWindow();
+    createTray();
+    // 创建菜单并传递回调函数
+    menuManager.createMenu(checkForUpdatesHandler);
+
+    app.on('activate', () => {
+        if (BrowserWindow.getAllWindows().length === 0) {
+            createWindow();
+        }
+    });
+
+    // 监听 before-quit 事件,关闭 IES.ExamServer.exe 进程
+    app.on('before-quit', async (event) => {
+        if (app.isQuitting) {
+            return; // 如果已经在退出流程中,则直接返回
+        }
+        app.isQuitting = true; // 标记正在退出
+        event.preventDefault(); // 阻止默认的退出行为
+        await serverManager.shutdownServer(); // 关闭服务器
+        app.quit(); // 触发退出流程
+    });
+});
+
+// 当所有窗口关闭时退出应用(macOS 除外)
+app.on('window-all-closed', function () {
+    if (process.platform !== 'darwin') {
+        app.quit();
+    }
+});

+ 3 - 2
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/constants.js

@@ -16,12 +16,13 @@ if (app.isPackaged) {
 } else {
     serverPath = __dirname;
 }
-
-const baseUrl = 'https://exam.habook.local:8888';
+const port = 8326;
+const baseUrl = 'https://exam.habook.local:8326';//端口含义:TEAM  键盘九宫格  T(8) E(3) A(2) M(6)
 const remoteVersionsUrl = 'https://teammodelos.blob.core.chinacloudapi.cn/0-public/exam-server/versions.json';
 const remoteZipBaseUrl = 'https://teammodelos.blob.core.chinacloudapi.cn/0-public/exam-server';
 
 module.exports = {
+    port,
     serverPath,
     baseUrl,
     remoteVersionsUrl,

文件差異過大導致無法顯示
+ 106 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/index.html


+ 203 - 73
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/main.js

@@ -7,53 +7,24 @@ const updateManager = require('./updateManager');
 const constants = require('./constants');
 const { getNetworkInterfaces } = require('./networkService');
 const { exec } = require('child_process');
- 
-//app.disableHardwareAcceleration(); //使用windows7 ,或者虚拟机的时候 需要验证 禁用 GPU 加速
-//app.commandLine.appendSwitch('ignore-certificate-errors')
+const net = require('net');
+const os = require('os');
+const utils = require('./utils');
+
 let win = null;
 let tray = null;
 app.isQuitting = false; // 添加标志位
+
+// 根据操作系统选择命令
+const IS_WINDOWS = os.platform() === 'win32';
+const FIND_PORT_COMMAND = IS_WINDOWS ? `netstat -ano | findstr :${constants.port}` : `lsof -i :${constants.port} -t`;
+const KILL_PROCESS_COMMAND = IS_WINDOWS  ? `taskkill /PID {PID} /F`  : `kill -9 {PID}`;
+
+
 // 创建 Electron 窗口的函数
 const createWindow = async () => {
     try {
-        const networks = getNetworkInterfaces();
-      //  console.log('Available networks:', networks);
-        console.log("运行地址:", networks[0].ip)
-        try {
-            let filePath = path.join(constants.serverPath,'certificate.bat');
-            filePath = `"${filePath}"`;
-            // 使用 execFile 执行 .bat 文件
-     
-            // 执行 .bat 文件
-
-            // 设置超时时间为 60 秒
-            exec(filePath,  (error, stdout, stderr) => {
-                if (error) {
-                    if (error.code === 'ETIMEDOUT') {
-                        console.error('执行 .bat 文件超时');
-                    } else {
-                        console.error(`执行 .bat 文件时出错: ${error.message}`);
-                    }
-                    return;
-                }
-                if (stderr) {
-                    console.error(`.bat 文件执行过程中出现错误: ${stderr}`);
-                    return;
-                }
-                console.log(`.bat 文件执行结果: ${stdout}`);
-            });
-        } catch (error)
-        {
-            console.log(`脚本启动执行错误: ${error}`);
-        }
 
-
-        console.log("开始检查是否启动服务...")
-        const isServerRunning = await serverManager.checkServerHealth();
-        if (!isServerRunning) {
-            console.log('Server is not running, starting it...');
-            await serverManager.startServer(); // 启动 Web API
-        }
         win = new BrowserWindow({
             width: 800,
             height: 600,
@@ -62,14 +33,19 @@ const createWindow = async () => {
                 contextIsolation: false,
             },
         });
+        win.webContents.openDevTools(); // 打开开发者工具
         //win.webContents.session.setCertificateVerifyProc((request, callback) => {
         //    // 始终返回 0 表示验证通过
         //    callback(0);
         //});///login/admin
         win.maximize();
-        win.loadURL(`${constants.baseUrl}/login/admin`, {
-            agent: constants.agent
-        });
+        win.loadFile('index.html');
+
+        // 模拟执行业务过程
+        StartProcess();
+        //win.loadURL(`${constants.baseUrl}/login/admin`, {
+        //    agent: constants.agent
+        //});
         // 监听窗口关闭事件,隐藏窗口而不是关闭
         win.on('close', (event) => {
             if (!app.isQuitting) {
@@ -77,13 +53,128 @@ const createWindow = async () => {
                 win.hide();
             }
         });
-
     } catch (error) {
         console.error('Error starting server or loading window:', error);
     }
 };
 
 
+
+// 当 Electron 应用准备好时创建窗口
+app.whenReady().then(() => {
+    process.env.NODE_OPTIONS = '--tls-min-v1.2';
+    createWindow();
+    createTray();
+    // 创建菜单并传递回调函数
+    menuManager.createMenu(checkForUpdatesHandler);
+
+    app.on('activate', () => {
+        if (BrowserWindow.getAllWindows().length === 0) {
+            createWindow();
+        }
+    });
+
+    // 监听 before-quit 事件,关闭 IES.ExamServer.exe 进程
+    app.on('before-quit', async (event) => {
+        if (app.isQuitting) {
+            return; // 如果已经在退出流程中,则直接返回
+        }
+        app.isQuitting = true; // 标记正在退出
+        event.preventDefault(); // 阻止默认的退出行为
+        await serverManager.shutdownServer(); // 关闭服务器
+        app.quit(); // 触发退出流程
+    });
+});
+
+// 当所有窗口关闭时退出应用(macOS 除外)
+app.on('window-all-closed', function () {
+    if (process.platform !== 'darwin') {
+        app.quit();
+    }
+});
+
+// 模拟执行业务过程
+async function StartProcess() {
+    //步骤1 开始检查
+    sendLogMessage('检查评测服务是否启动...');
+    //步骤2 检查端口是否占用。
+    let serverStarted = false;
+    try {
+        const inUse = await isPortInUse(constants.port);
+        if (inUse) {
+            serverStarted = true;
+            sendLogMessage(`检查到端口被占用...`);
+
+        } else {
+            sendLogMessage(`评测服务未启动...`);
+        }
+    } catch (err) {
+        sendLogMessage(`检查端口时出错...`);
+      //  console.log(`检查评测服务状态出错...`, err);
+    }
+    //步骤3,尝试优雅关闭,如果不能关闭再通过 关闭进程号关闭端口
+    let needStart = false;
+    if (serverStarted) {
+        //检测是否是.net6的服务在线
+        sendLogMessage('检查是否是评测服务占用端口...');
+        const isServerRunning = await serverManager.checkServerHealth();
+        if (!isServerRunning) {
+            //可能是其他程序占用
+            sendLogMessage('检测到其他程序占用端口...');
+            sendLogMessage('正在查找占用端口的进程...');
+            const pid = await findProcessUsingPort();
+            sendLogMessage(`找到占用端口的进程: ${pid}`);
+            sendLogMessage('正在关闭进程...');
+            const killResult = await killProcess(pid);
+            sendLogMessage(`进程关闭结果:${killResult}`);
+            utils.delay(500);
+            needStart = true;
+        } else {
+            sendLogMessage('评测服务正在运行中...');
+            sendLogMessage('如需强制退出,请点击<设置><退出>按钮...');
+        }
+    } else {
+        needStart = true;
+    }
+    if (needStart) {
+        sendLogMessage('正在启动评测服务...');
+        await serverManager.startServer();
+        const isServerRunning = await serverManager.checkServerHealth();
+        if (isServerRunning) {
+            sendLogMessage('本地IP域名映射配置成功...');
+            sendLogMessage('SSL安全证书安装成功...');
+            sendLogMessage('评测服务启动成功...');
+            sendLogMessage('正在加载登录页面...');
+            utils.delay(2000)
+            win.loadURL(`${constants.baseUrl}/login/admin`, {
+                agent: constants.agent
+            });
+
+        }
+        else {
+            sendLogMessage('评测服务启动失败...');
+            sendLogMessage('请检查hosts文件是否自动映射了exam.habook.local域名...');
+            sendLogMessage("如果没有请手动执行<teacher_manual.bat>脚本文件...");
+        }
+    } else
+    {
+        sendLogMessage('正在加载登录页面...');
+        utils.delay(2000)
+        win.loadURL(`${constants.baseUrl}/login/admin`, {
+            agent: constants.agent
+        });
+    }
+  
+}
+
+
+// 发送消息到渲染进程
+function sendLogMessage(message) {
+    if (win) {
+        win.webContents.send('log-message', message);
+    }
+}
+
 const createTray = () => {
     const iconPath = path.join(__dirname, 'logo.ico'); // 你的托盘图标路径
     tray = new Tray(iconPath);
@@ -122,44 +213,83 @@ const createTray = () => {
     });
 };
 
-
-
-
 // 定义回调函数
 const checkForUpdatesHandler = () => {
     updateManager.checkForUpdates(win, () => {
         menuManager.createMenu(checkForUpdatesHandler); // 重新创建菜单并传递回调函数
     });
 };
-// 当 Electron 应用准备好时创建窗口
-app.whenReady().then(() => {
-    process.env.NODE_OPTIONS = '--tls-min-v1.2';
-    createWindow();
-    createTray();
-    // 创建菜单并传递回调函数
-    menuManager.createMenu(checkForUpdatesHandler);
+function isPortInUse(port) {
+    return new Promise((resolve, reject) => {
+        const server = net.createServer()
+            .once('error', (err) => {
+                if (err.code === 'EADDRINUSE') {
+                    resolve(true); // 端口被占用
+                } else {
+                    reject(err);
+                }
+            })
+            .once('listening', () => {
+                server.close();
+                resolve(false); // 端口未被占用
+            })
+            .listen(port);
+    });
+}
 
-    app.on('activate', () => {
-        if (BrowserWindow.getAllWindows().length === 0) {
-            createWindow();
-        }
+// 查找占用端口的进程
+function findProcessUsingPort() {
+    return new Promise((resolve, reject) => {
+        exec(FIND_PORT_COMMAND, (error, stdout, stderr) => {
+            if (error) {
+                if (error.code === 1) {
+                    // 未找到占用端口的进程
+                    resolve(null);
+                } else {
+                    reject(`查找端口占用失败: ${stderr}`);
+                }
+                return;
+            }
+            // 提取 PID
+            const pid = IS_WINDOWS
+                ? stdout.trim().split(/\s+/).pop() // Windows: 取最后一列
+                : stdout.trim(); // macOS/Linux: 直接输出 PID
+            resolve(pid);
+        });
     });
+}
 
-    // 监听 before-quit 事件,关闭 IES.ExamServer.exe 进程
-    app.on('before-quit', async (event) => {
-        if (app.isQuitting) {
-            return; // 如果已经在退出流程中,则直接返回
+// 关闭进程
+function killProcess(pid) {
+    return new Promise((resolve, reject) => {
+        if (!pid) {
+            resolve('未找到占用端口的进程');
+            return;
         }
-        app.isQuitting = true; // 标记正在退出
-        event.preventDefault(); // 阻止默认的退出行为
-        await serverManager.shutdownServer(); // 关闭服务器
-        app.quit(); // 触发退出流程
+        const command = KILL_PROCESS_COMMAND.replace('{PID}', pid);
+        exec(command, (error, stdout, stderr) => {
+            if (error) {
+                reject(`关闭进程失败: ${stderr}`);
+                return;
+            }
+            resolve(`已关闭进程: ${pid}`);
+        });
     });
-});
+}
+
+//// 启动 .NET Core 应用程序
+//function startDotNetApp() {
+//    return new Promise((resolve, reject) => {
+//        const command = `dotnet run --urls=http://localhost:${PORT}`;
+//        const options = { cwd: DOTNET_PROJECT_PATH }; // 设置工作目录为 .NET 项目路径
+//        exec(command, options, (error, stdout, stderr) => {
+//            if (error) {
+//                reject(`启动 .NET Core 应用程序失败: ${stderr}`);
+//                return;
+//            }
+//            resolve(`.NET Core 应用程序已启动: ${stdout}`);
+//        });
+//    });
+//}
+
 
-// 当所有窗口关闭时退出应用(macOS 除外)
-app.on('window-all-closed', function () {
-    if (process.platform !== 'darwin') {
-        app.quit();
-    }
-});

+ 3 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/package.json

@@ -49,6 +49,9 @@
           "**/*"
         ]
       }
+    ],
+    "extraResources": [
+      "index.html"
     ]
   },
   "dependencies": {

+ 9 - 3
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/serverManager.js

@@ -41,7 +41,8 @@ const startServer = () => {
             const checkHealth = async () => {
                 try {
                     const response = await axios.get(`${constants.baseUrl}/index/health`, {
-                        httpsAgent: constants.agent
+                        httpsAgent: constants.agent,
+                        timeout: 5000 // 设置超时时间为 5 秒
                     });
                     if (response.status === 200) {
                         console.log('Server is up and running!');
@@ -49,7 +50,7 @@ const startServer = () => {
                     }
                 } catch (error) {
                     console.log('Waiting for server to start...', error);
-                    setTimeout(checkHealth, 1000); // 每隔 1 秒检查一次
+                    setTimeout(checkHealth, 10000); // 每隔 10 秒检查一次
                 }
             };
             checkHealth();
@@ -84,8 +85,13 @@ const shutdownServer = async () => {
     try {
         console.log('index/shutdown api ...');
         const response = await axios.get(`${constants.baseUrl}/index/shutdown?delay=0`, {
-            httpsAgent: constants.agent
+            httpsAgent: constants.agent,
+            validateStatus: (status) => status === 200 || status === 404, // 允许 404 状态码
+            timeout: 5000 // 设置超时时间为 5 秒
         });
+        if (response.status === 404) {
+            console.error('API: index/shutdown not found.');
+        }
         if (response.status === 200) {
             console.log('Server is shutdown!');
         }

+ 1 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/updateManager.js

@@ -139,7 +139,7 @@ const updateServer = async (latestVersion, win, createMenuCallback) => {
         try {
             // 2. 关闭 IES.ExamServer.exe
             console.log('Shutting down IES.ExamServer...');
-            const response = await axios.get(`${constants.baseUrl}/index/shutdown`, {
+            const response = await axios.get(`${constants.baseUrl}/index/shutdown?delay=0`, {
                 httpsAgent: constants.agent,
                 validateStatus: (status) => status === 200 || status === 404, // 允许 404 状态码
                 timeout: 5000 // 设置超时时间为 5 秒

+ 1 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/appsettings.json

@@ -17,7 +17,7 @@
   "Kestrel": {
     "Endpoints": {
       "Https1": {
-        "Url": "https://*:8888",
+        "Url": "https://*:8326",
         "Certificate": {
           "Path": "Configs/cer/cert.pem",
           "KeyPath": "Configs/cer/key.pem"