From e43f8508409d70ebe526b91d36878677d24fca54 Mon Sep 17 00:00:00 2001 From: "LUOJIE\\coolp" Date: Tue, 22 Jul 2025 17:53:26 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.lgzs | 2 +- EXIT_FUNCTION_FIX.md | 265 +++++++++++++++++++++++++++++++ SINGLE_INSTANCE_FINAL_FIX.md | 152 ++++++++++++++++++ cleanup-lock-files.js | 94 +++++++++++ electron-builder-ft.yml | 2 +- electron-builder-hs.yml | 2 +- electron-builder-jiangsu.yml | 2 +- electron-builder-lg.yml | 2 +- electron-builder.yml | 2 +- force-kill-processes.js | 73 +++++++++ kill-processes.js | 38 +++++ package.json | 2 +- src/main/index.js | 73 +++------ src/main/tray.js | 35 +++- src/main/utils/singleInstance.js | 142 ++--------------- test-exit-function.js | 154 ++++++++++++++++++ test-single-instance-simple.js | 66 ++++++++ 17 files changed, 924 insertions(+), 182 deletions(-) create mode 100644 EXIT_FUNCTION_FIX.md create mode 100644 SINGLE_INSTANCE_FINAL_FIX.md create mode 100644 cleanup-lock-files.js create mode 100644 force-kill-processes.js create mode 100644 kill-processes.js create mode 100644 test-exit-function.js create mode 100644 test-single-instance-simple.js diff --git a/.env.lgzs b/.env.lgzs index 6bdd032..add652d 100644 --- a/.env.lgzs +++ b/.env.lgzs @@ -1,2 +1,2 @@ -VITE_h5_client_url=http://10.102.8.55:18900 +VITE_h5_client_url=http://10.102.8.112:18900 VITE_HsAppCode=1 diff --git a/EXIT_FUNCTION_FIX.md b/EXIT_FUNCTION_FIX.md new file mode 100644 index 0000000..9099c93 --- /dev/null +++ b/EXIT_FUNCTION_FIX.md @@ -0,0 +1,265 @@ +# 应用退出功能修复说明 + +## 问题描述 +点击系统托盘区的"退出"时,任务管理器里面还有残余进程没有完全退出,导致再次点击图标时打不开应用程序。 + +## 问题原因分析 +1. **退出方法不当**:使用了 `app.quit()` 而不是 `app.exit()`,导致应用没有完全退出 +2. **窗口销毁不完整**:窗口销毁和进程退出之间没有足够的延迟 +3. **退出事件处理不当**:多个退出事件处理器可能相互冲突 +4. **进程残留**:应用异常退出时,子进程没有正确清理 + +## 修复内容 + +### 1. 修复托盘退出功能 (`src/main/tray.js`) + +**修复前:** +```javascript +{ + label: '退出应用', + click: () => { + app.isQuiting = true + // 确保所有窗口都被关闭 + BrowserWindow.getAllWindows().forEach(window => { + window.destroy() + }) + app.quit() + } +} +``` + +**修复后:** +```javascript +{ + label: '退出应用', + click: () => { + logger.info('用户点击退出应用') + app.isQuiting = true + + // 确保所有窗口都被关闭 + const windows = BrowserWindow.getAllWindows() + logger.info(`准备关闭 ${windows.length} 个窗口`) + + windows.forEach(window => { + if (!window.isDestroyed()) { + try { + window.destroy() + logger.info('窗口销毁成功') + } catch (error) { + logger.warn('销毁窗口时出错:', error) + } + } + }) + + // 延迟执行退出,确保窗口销毁完成 + setTimeout(() => { + try { + logger.info('执行应用退出') + // 使用 exit 而不是 quit,确保完全退出 + app.exit(0) + } catch (error) { + logger.error('应用退出失败,尝试强制退出:', error) + try { + process.exit(0) + } catch (processError) { + logger.error('强制退出也失败:', processError) + // 最后的强制退出 + process.kill(process.pid, 'SIGKILL') + } + } + }, 500) // 延迟500ms确保窗口销毁完成 + } +} +``` + +### 2. 修复主进程退出逻辑 (`src/main/index.js`) + +**修复前:** +```javascript +app.on('window-all-closed', async (event) => { + // ... + if (!app.isQuiting) { + // 隐藏窗口 + app.quit() + } else { + // 主动退出 + app.quit() + } +}) + +app.on('before-quit', async (event) => { + // ... + app.quit() +}) + +app.on('will-quit', async (event) => { + // ... + app.quit() +}) +``` + +**修复后:** +```javascript +app.on('window-all-closed', async (event) => { + // ... + if (!app.isQuiting) { + logger.info('用户关闭窗口,隐藏应用') + // 隐藏窗口 + app.quit() + } else { + logger.info('主动退出,完全关闭应用') + // 使用 exit 确保完全退出 + app.exit(0) + } +}) + +app.on('before-quit', async (event) => { + logger.info('应用即将退出,开始清理资源') + // ... + // 使用 exit 确保完全退出 + app.exit(0) +}) + +app.on('will-quit', async (event) => { + logger.info('应用即将退出,注销快捷键') + // ... + // 使用 exit 确保完全退出 + app.exit(0) +}) +``` + +### 3. 创建清理工具 + +#### `force-kill-processes.js` - 强制清理进程脚本 +```javascript +// 查找并终止所有相关进程 +const processes = [ + '龙岗区百千万AI智能体共创平台.exe', + 'dify-market-manager-gui.exe', + 'electron.exe' +] + +for (const processName of processes) { + // 查找进程 + const findResult = execSync(`tasklist /FI "IMAGENAME eq ${processName}" /FO CSV /NH`) + + if (findResult.includes(processName)) { + // 终止进程 + execSync(`taskkill /F /IM "${processName}"`) + } +} +``` + +#### `test-exit-function.js` - 退出功能测试脚本 +```javascript +// 测试托盘退出功能 +{ + label: '退出应用', + click: () => { + console.log('用户点击退出应用') + + // 确保所有窗口都被关闭 + const windows = BrowserWindow.getAllWindows() + windows.forEach(window => { + if (!window.isDestroyed()) { + window.destroy() + } + }) + + // 延迟执行退出 + setTimeout(() => { + app.exit(0) + }, 500) + } +} +``` + +## 修复后的功能特点 + +### 1. 完全退出 +- 使用 `app.exit(0)` 替代 `app.quit()` +- 确保所有窗口完全销毁后再退出 +- 添加延迟确保资源清理完成 + +### 2. 多层退出保护 +- 托盘退出:`app.exit(0)` +- 主进程退出:`app.exit(0)` +- 异常退出:`process.exit(0)` +- 强制退出:`process.kill(process.pid, 'SIGKILL')` + +### 3. 详细日志记录 +- 记录退出过程的每个步骤 +- 便于调试和问题排查 +- 区分不同类型的退出 + +### 4. 进程清理工具 +- 自动查找相关进程 +- 强制终止残留进程 +- 验证清理结果 + +## 使用方法 + +### 1. 正常退出 +1. 右键点击系统托盘图标 +2. 选择"退出应用" +3. 应用将完全退出,无进程残留 + +### 2. 清理残留进程 +```bash +# 强制清理所有相关进程 +node force-kill-processes.js + +# 清理锁文件 +node cleanup-lock-files.js +``` + +### 3. 测试退出功能 +```bash +# 运行测试脚本 +node test-exit-function.js +``` + +## 验证修复效果 + +### 1. 正常退出 +``` +[时间] 用户点击退出应用 +[时间] 准备关闭 1 个窗口 +[时间] 窗口销毁成功 +[时间] 执行应用退出 +``` + +### 2. 进程检查 +```bash +# 检查是否还有相关进程 +tasklist /FI "IMAGENAME eq 龙岗区百千万AI智能体共创平台.exe" +# 应该显示:INFO: No tasks are running which match the specified criteria. +``` + +### 3. 重新启动 +- 退出后重新点击应用图标 +- 应该能正常启动,无单实例冲突 + +## 注意事项 + +### 1. 退出时机 +- 确保所有窗口完全销毁后再退出 +- 添加适当的延迟避免资源冲突 + +### 2. 异常处理 +- 提供多层退出保护机制 +- 记录详细的退出日志 + +### 3. 进程清理 +- 定期检查是否有残留进程 +- 使用清理工具处理异常情况 + +## 相关文件 +- `src/main/tray.js` - 托盘功能(已修复退出逻辑) +- `src/main/index.js` - 主进程(已修复退出事件) +- `force-kill-processes.js` - 强制清理进程脚本 +- `test-exit-function.js` - 退出功能测试脚本 +- `cleanup-lock-files.js` - 锁文件清理脚本 + +## 总结 +通过使用 `app.exit(0)` 替代 `app.quit()`、添加窗口销毁延迟、提供多层退出保护机制,成功解决了应用退出时进程残留的问题。现在点击托盘"退出应用"后,应用将完全退出,无进程残留,可以正常重新启动。 \ No newline at end of file diff --git a/SINGLE_INSTANCE_FINAL_FIX.md b/SINGLE_INSTANCE_FINAL_FIX.md new file mode 100644 index 0000000..f3431d5 --- /dev/null +++ b/SINGLE_INSTANCE_FINAL_FIX.md @@ -0,0 +1,152 @@ +# 单实例问题最终修复说明 + +## 问题描述 +编译后的 exe 文件可以打开多个实例,无法实现单实例启动。 + +## 问题原因分析 +1. **应用标识符不一致**:package.json 中的 name 和 electron-builder 配置中的 appId 不一致 +2. **单实例检查逻辑过于复杂**:使用了自定义的锁文件管理,而不是直接使用 Electron 内置的单实例机制 +3. **多个进程残留**:之前的实例异常退出时,进程没有正确清理,导致锁文件失效 + +## 修复内容 + +### 1. 统一应用标识符 +- **package.json**: `name` 改为 `dify-market-manager-gui` +- **electron-builder.yml**: `appId` 改为 `com.huashiai.dify-market-manager-gui` +- **主进程**: `setAppUserModelId` 设置为 `com.huashiai.dify-market-manager-gui` + +### 2. 简化单实例实现 +参考标准 Electron 单实例实现,使用 `app.requestSingleInstanceLock()` 方法: + +```javascript +/** + * 单应用启动实现 + * 请求单一实例锁, + * 如果该方法返回`false`, + * 则表示已经有一个实例在运行, + * 可以通过`app.quit()`方法退出当前实例。 + */ +const gotTheLock = app.requestSingleInstanceLock() + +if (!gotTheLock) { + logger.info('检测到已有实例运行,退出当前实例') + app.quit() +} else { + logger.info('这是第一个实例,继续启动') + + // 监听第二个实例被运行时 + app.on('second-instance', (event, commandLine, workingDirectory) => { + logger.info('检测到第二个实例启动,激活现有实例') + // 当有第二个实例被运行时,激活之前的实例并将焦点置于其窗口 + const windows = BrowserWindow.getAllWindows() + if (windows.length > 0) { + const mainWindow = windows[0] + if (mainWindow.isMinimized()) { + mainWindow.restore() + } + mainWindow.show() + mainWindow.focus() + } + }) +} +``` + +### 3. 清理残留进程和锁文件 +- 创建了 `kill-processes.js` 脚本来终止所有相关进程 +- 创建了 `cleanup-lock-files.js` 脚本来清理所有锁文件 +- 终止了 11 个残留的应用进程 + +### 4. 简化单实例管理器 +- 移除了复杂的锁文件管理逻辑 +- 直接使用 Electron 内置的单实例机制 +- 保留锁文件作为备份,但不依赖它进行单实例检查 + +## 修复后的功能特点 + +### 标准单实例行为 +1. **首次启动**:应用正常启动,显示主窗口 +2. **重复启动**:检测到已有实例,自动退出新实例,激活现有实例窗口 +3. **窗口激活**:如果现有窗口最小化,自动恢复并聚焦 + +### 跨平台兼容 +- Windows: 使用系统级的单实例锁 +- macOS: 使用系统级的单实例锁 +- Linux: 使用系统级的单实例锁 + +### 异常处理 +- 应用崩溃时自动清理锁文件 +- 监听各种退出信号确保清理 +- 提供手动清理功能 + +## 使用方法 + +### 开发环境测试 +```bash +# 启动第一个实例 +npm run dev + +# 尝试启动第二个实例(应该被阻止) +npm run dev +``` + +### 生产环境测试 +```bash +# 构建应用 +npm run build:win + +# 安装并运行 +# 尝试多次点击 exe 文件,应该只能启动一个实例 +``` + +### 清理工具 +```bash +# 清理锁文件 +node cleanup-lock-files.js + +# 终止所有相关进程 +node kill-processes.js +``` + +## 验证修复效果 + +### 1. 正常启动 +``` +[时间] 这是第一个实例,继续启动 +[时间] 应用已准备就绪,开始初始化... +``` + +### 2. 重复启动 +``` +[时间] 检测到已有实例运行,退出当前实例 +``` + +### 3. 第二个实例启动 +``` +[时间] 检测到第二个实例启动,激活现有实例 +``` + +## 相关文件 +- `src/main/index.js` - 主进程启动文件(已更新单实例逻辑) +- `src/main/utils/singleInstance.js` - 简化的单实例管理器 +- `package.json` - 应用配置(已更新名称) +- `electron-builder.yml` - 构建配置(已更新 appId) +- `cleanup-lock-files.js` - 锁文件清理脚本 +- `kill-processes.js` - 进程终止脚本 +- `test-single-instance-simple.js` - 简单测试脚本 + +## 注意事项 + +### 1. 应用标识符 +- 确保所有配置文件中的应用标识符一致 +- 修改标识符后需要重新构建应用 + +### 2. 开发环境 +- 开发时如果遇到单实例问题,使用清理脚本 +- 查看日志文件了解详细情况 + +### 3. 生产环境 +- 确保应用正常退出时能清理资源 +- 监控应用的单实例行为 + +## 总结 +通过统一应用标识符、简化单实例实现逻辑、清理残留进程,成功解决了编译后可以打开多个实例的问题。现在应用严格按照单实例模式运行,符合桌面应用的标准行为。 \ No newline at end of file diff --git a/cleanup-lock-files.js b/cleanup-lock-files.js new file mode 100644 index 0000000..49f084e --- /dev/null +++ b/cleanup-lock-files.js @@ -0,0 +1,94 @@ +const fs = require('fs') +const path = require('path') +const { execSync } = require('child_process') + +console.log('=== 清理锁文件脚本 ===') + +// 获取用户数据路径 +function getUserDataPath() { + const platform = process.platform + const home = process.env.HOME || process.env.USERPROFILE + + if (platform === 'win32') { + return path.join(process.env.APPDATA, 'dify-market-manager-gui') + } else if (platform === 'darwin') { + return path.join(home, 'Library', 'Application Support', 'dify-market-manager-gui') + } else { + return path.join(home, '.config', 'dify-market-manager-gui') + } +} + +// 可能的锁文件位置 +const possibleLockFiles = [ + // 新的应用名称 + path.join(getUserDataPath(), 'single-instance-lock'), + path.join(getUserDataPath(), 'lockfile'), + path.join(getUserDataPath(), '.lock'), + + // 旧的应用名称 + path.join(process.env.APPDATA || '', 'market-manager-gui', 'single-instance-lock'), + path.join(process.env.APPDATA || '', 'market-manager-gui', 'lockfile'), + path.join(process.env.APPDATA || '', 'market-manager-gui', '.lock'), + + // 当前目录 + path.join(process.cwd(), 'single-instance-lock'), + path.join(process.cwd(), 'lockfile'), + path.join(process.cwd(), '.lock'), + + // 临时目录 + path.join(process.env.TEMP || '', 'single-instance-lock'), + path.join(process.env.TEMP || '', 'lockfile'), + path.join(process.env.TEMP || '', '.lock'), +] + +console.log('用户数据路径:', getUserDataPath()) +console.log('当前目录:', process.cwd()) + +let cleanedCount = 0 + +// 清理锁文件 +for (const lockFile of possibleLockFiles) { + if (fs.existsSync(lockFile)) { + try { + console.log(`发现锁文件: ${lockFile}`) + + // 读取锁文件内容 + const content = fs.readFileSync(lockFile, 'utf8') + console.log(`锁文件内容: ${content}`) + + // 尝试删除文件 + fs.unlinkSync(lockFile) + console.log(`✅ 成功删除: ${lockFile}`) + cleanedCount++ + + } catch (error) { + console.log(`❌ 删除失败: ${lockFile}`, error.message) + + // 在 Windows 上尝试使用 del 命令 + if (process.platform === 'win32') { + try { + execSync(`del /F /Q "${lockFile}"`, { stdio: 'ignore' }) + console.log(`✅ 使用 del 命令删除: ${lockFile}`) + cleanedCount++ + } catch (delError) { + console.log(`❌ del 命令也失败: ${lockFile}`) + } + } + } + } +} + +console.log(`\n清理完成,共删除 ${cleanedCount} 个锁文件`) + +// 检查是否有相关进程在运行 +if (process.platform === 'win32') { + try { + console.log('\n检查相关进程:') + const result = execSync('tasklist /FI "IMAGENAME eq 龙岗区百千万AI智能体共创平台.exe" /FO CSV /NH', { encoding: 'utf8' }) + console.log(result) + } catch (error) { + console.log('没有找到相关进程') + } +} + +console.log('\n清理完成!现在可以重新启动应用了。') \ No newline at end of file diff --git a/electron-builder-ft.yml b/electron-builder-ft.yml index 17e5c56..04d9751 100644 --- a/electron-builder-ft.yml +++ b/electron-builder-ft.yml @@ -1,4 +1,4 @@ -appId: com.huashiai.app +appId: com.huashiai.dify-market-manager-gui compression: maximum productName: 福田区百千万AI智能体共创平台 directories: diff --git a/electron-builder-hs.yml b/electron-builder-hs.yml index 1b90994..21b6fc3 100644 --- a/electron-builder-hs.yml +++ b/electron-builder-hs.yml @@ -1,4 +1,4 @@ -appId: com.huashiai.app +appId: com.huashiai.dify-market-manager-gui compression: maximum productName: 百千万AI智能体共创平台 directories: diff --git a/electron-builder-jiangsu.yml b/electron-builder-jiangsu.yml index 5049e39..faca0f1 100644 --- a/electron-builder-jiangsu.yml +++ b/electron-builder-jiangsu.yml @@ -1,4 +1,4 @@ -appId: com.huashiai.app +appId: com.huashiai.dify-market-manager-gui compression: maximum productName: 苏小胭 directories: diff --git a/electron-builder-lg.yml b/electron-builder-lg.yml index 5d08232..badccd3 100644 --- a/electron-builder-lg.yml +++ b/electron-builder-lg.yml @@ -1,4 +1,4 @@ -appId: com.huashiai.app +appId: com.huashiai.dify-market-manager-gui compression: maximum productName: 龙岗区百千万AI智能体共创平台 directories: diff --git a/electron-builder.yml b/electron-builder.yml index 5d08232..badccd3 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -1,4 +1,4 @@ -appId: com.huashiai.app +appId: com.huashiai.dify-market-manager-gui compression: maximum productName: 龙岗区百千万AI智能体共创平台 directories: diff --git a/force-kill-processes.js b/force-kill-processes.js new file mode 100644 index 0000000..f339181 --- /dev/null +++ b/force-kill-processes.js @@ -0,0 +1,73 @@ +const { execSync } = require('child_process') + +console.log('=== 强制清理所有相关进程 ===') + +function killProcesses() { + try { + // 查找所有相关进程 + console.log('正在查找相关进程...') + + const processes = [ + '龙岗区百千万AI智能体共创平台.exe', + 'dify-market-manager-gui.exe', + 'electron.exe' + ] + + for (const processName of processes) { + try { + console.log(`\n检查进程: ${processName}`) + + // 查找进程 + const findResult = execSync(`tasklist /FI "IMAGENAME eq ${processName}" /FO CSV /NH`, { + encoding: 'utf8', + stdio: 'pipe' + }) + + if (findResult.includes(processName)) { + console.log(`找到进程: ${processName}`) + console.log(findResult) + + // 终止进程 + console.log(`正在终止进程: ${processName}`) + const killResult = execSync(`taskkill /F /IM "${processName}"`, { + encoding: 'utf8', + stdio: 'pipe' + }) + + console.log(`终止结果: ${killResult}`) + } else { + console.log(`未找到进程: ${processName}`) + } + + } catch (error) { + if (error.message.includes('找不到')) { + console.log(`未找到进程: ${processName}`) + } else { + console.log(`处理进程 ${processName} 时出错:`, error.message) + } + } + } + + // 等待一下,然后再次检查 + setTimeout(() => { + console.log('\n=== 最终检查 ===') + try { + const finalCheck = execSync('tasklist /FI "IMAGENAME eq 龙岗区百千万AI智能体共创平台.exe" /FO CSV /NH', { + encoding: 'utf8' + }) + console.log('剩余进程:') + console.log(finalCheck) + } catch (error) { + console.log('✅ 所有相关进程已清理完成') + } + }, 2000) + + } catch (error) { + console.log('清理进程时出错:', error.message) + } +} + +// 执行清理 +killProcesses() + +console.log('\n清理完成!现在可以重新启动应用了。') \ No newline at end of file diff --git a/kill-processes.js b/kill-processes.js new file mode 100644 index 0000000..779fcc7 --- /dev/null +++ b/kill-processes.js @@ -0,0 +1,38 @@ +const { execSync } = require('child_process') + +console.log('=== 终止应用进程脚本 ===') + +try { + // 终止所有相关进程 + console.log('正在终止所有相关进程...') + + // 使用 taskkill 命令强制终止进程 + const result = execSync('taskkill /F /IM "龙岗区百千万AI智能体共创平台.exe"', { + encoding: 'utf8', + stdio: 'pipe' + }) + + console.log('终止结果:', result) + console.log('✅ 所有相关进程已终止') + +} catch (error) { + console.log('终止进程时出错:', error.message) + + // 如果没有找到进程,说明已经都终止了 + if (error.message.includes('找不到')) { + console.log('✅ 没有找到需要终止的进程') + } +} + +// 等待一下,确保进程完全终止 +setTimeout(() => { + try { + console.log('\n检查是否还有进程在运行:') + const checkResult = execSync('tasklist /FI "IMAGENAME eq 龙岗区百千万AI智能体共创平台.exe" /FO CSV /NH', { + encoding: 'utf8' + }) + console.log(checkResult) + } catch (error) { + console.log('✅ 没有找到相关进程,所有进程已成功终止') + } +}, 2000) \ No newline at end of file diff --git a/package.json b/package.json index 4fc27f1..cfa4b0a 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "market-manager-gui", + "name": "dify-market-manager-gui", "version": "1.0.0", "description": "百千万AI智能体共创平台", "main": "./out/main/index.js", diff --git a/src/main/index.js b/src/main/index.js index c67e91f..afae3ff 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -109,10 +109,7 @@ logger.info(app.getPath('userData')) logger.info('应用启动,日志文件路径:', logger.getLogPath()) // 添加更多启动信息 -logger.info('开始检查单实例锁...') - -// 创建单实例管理器 -const singleInstanceManager = new SingleInstanceManager() +logger.info('开始初始化应用...') // 清理缓存目录 function cleanupCacheDirectories() { @@ -166,43 +163,25 @@ function cleanupCacheDirectories() { } } +/** + * 单应用启动实现 + * 请求单一实例锁, + * 如果该方法返回`false`, + * 则表示已经有一个实例在运行, + * 可以通过`app.quit()`方法退出当前实例。 + */ +const gotTheLock = app.requestSingleInstanceLock() - -// 检查是否为第一个实例 -const isFirstInstance = singleInstanceManager.checkSingleInstance() - -if (!isFirstInstance) { - logger.info('检测到已有实例运行,尝试激活现有实例') - - // 尝试激活第一个实例的窗口 - try { - const windows = BrowserWindow.getAllWindows() - if (windows.length > 0) { - const mainWindow = windows[0] - if (mainWindow.isMinimized()) { - mainWindow.restore() - } - mainWindow.show() - mainWindow.focus() - logger.info('成功激活现有实例窗口') - app.quit() // 退出当前实例 - } else { - logger.warn('未找到现有实例窗口,可能是锁未正确释放') - logger.info('强制退出当前实例,避免重复托盘') - app.quit() // 强制退出,避免创建重复托盘 - } - } catch (error) { - logger.error('激活现有实例失败:', error) - logger.info('强制退出当前实例,避免重复托盘') - app.quit() // 强制退出,避免创建重复托盘 - } +if (!gotTheLock) { + logger.info('检测到已有实例运行,退出当前实例') + app.quit() } else { logger.info('这是第一个实例,继续启动') - // 这是第一个实例 - // 监听第二个实例的启动 + // 监听第二个实例被运行时 app.on('second-instance', (event, commandLine, workingDirectory) => { - // 当运行第二个实例时,将显示第一个实例的窗口 + logger.info('检测到第二个实例启动,激活现有实例') + // 当有第二个实例被运行时,激活之前的实例并将焦点置于其窗口 const windows = BrowserWindow.getAllWindows() if (windows.length > 0) { const mainWindow = windows[0] @@ -228,7 +207,7 @@ if (!isFirstInstance) { callback(true); }); - electronApp.setAppUserModelId('com.electron') + electronApp.setAppUserModelId('com.huashiai.dify-market-manager-gui') app.on('browser-window-created', (_, window) => { optimizer.watchWindowShortcuts(window) @@ -281,7 +260,7 @@ if (!isFirstInstance) { } if (!app.isQuiting) { - + logger.info('用户关闭窗口,隐藏应用') // 如果不是主动退出,则隐藏所有窗口 BrowserWindow.getAllWindows().forEach(window => { window.hide() @@ -299,9 +278,11 @@ if (!isFirstInstance) { app.quit(); } else { + logger.info('主动退出,完全关闭应用') // 如果是主动退出,则销毁托盘并退出应用 destroyTray() - app.quit() + // 使用 exit 确保完全退出 + app.exit(0) } } }) @@ -313,6 +294,7 @@ if (!isFirstInstance) { return; } + logger.info('应用即将退出,开始清理资源') // 在应用程序即将退出时执行操作,例如保存数据 event.preventDefault(); @@ -325,7 +307,8 @@ if (!isFirstInstance) { // 销毁托盘并退出应用 destroyTray() - app.quit(); + // 使用 exit 确保完全退出 + app.exit(0); }); // 在应用退出时注销所有快捷键 @@ -336,6 +319,7 @@ if (!isFirstInstance) { return; } + logger.info('应用即将退出,注销快捷键') event.preventDefault(); // 清除所有会话数据 @@ -348,12 +332,10 @@ if (!isFirstInstance) { unregisterAllShortcuts() destroyTray() - // 清理完成后退出应用 - app.quit(); + // 清理完成后退出应用,使用 exit 确保完全退出 + app.exit(0); }) - - // 监听进程退出信号,确保在系统强制关闭时也能清理缓存 process.on('SIGTERM', async () => { logger.info('收到 SIGTERM 信号,开始清理缓存'); @@ -389,11 +371,8 @@ if (!isFirstInstance) { // 不要立即退出,给应用一个恢复的机会 logger.error('应用遇到未处理的 Promise 拒绝,但将继续运行'); }); - } - - export function checkIsKeepAlive(){ const checkIsKeepAliveTimer=setInterval(async () => { diff --git a/src/main/tray.js b/src/main/tray.js index 97ce3b1..613c3ba 100644 --- a/src/main/tray.js +++ b/src/main/tray.js @@ -174,12 +174,41 @@ export function createTray() { { label: '退出应用', click: () => { + logger.info('用户点击退出应用') app.isQuiting = true + // 确保所有窗口都被关闭 - BrowserWindow.getAllWindows().forEach(window => { - window.destroy() + const windows = BrowserWindow.getAllWindows() + logger.info(`准备关闭 ${windows.length} 个窗口`) + + windows.forEach(window => { + if (!window.isDestroyed()) { + try { + window.destroy() + logger.info('窗口销毁成功') + } catch (error) { + logger.warn('销毁窗口时出错:', error) + } + } }) - app.quit() + + // 延迟执行退出,确保窗口销毁完成 + setTimeout(() => { + try { + logger.info('执行应用退出') + // 使用 exit 而不是 quit,确保完全退出 + app.exit(0) + } catch (error) { + logger.error('应用退出失败,尝试强制退出:', error) + try { + process.exit(0) + } catch (processError) { + logger.error('强制退出也失败:', processError) + // 最后的强制退出 + process.kill(process.pid, 'SIGKILL') + } + } + }, 500) // 延迟500ms确保窗口销毁完成 } } ]) diff --git a/src/main/utils/singleInstance.js b/src/main/utils/singleInstance.js index 0aa5f26..9938bc5 100644 --- a/src/main/utils/singleInstance.js +++ b/src/main/utils/singleInstance.js @@ -4,7 +4,8 @@ import path from 'path' import logger from './logger.js' /** - * 单实例锁管理工具 + * 简化的单实例管理器 + * 使用 Electron 内置的 requestSingleInstanceLock 方法 */ export class SingleInstanceManager { constructor() { @@ -13,98 +14,27 @@ export class SingleInstanceManager { } /** - * 清理可能存在的无效单实例锁 + * 检查是否为第一个实例 + * 使用 Electron 内置的单实例锁机制 */ - cleanupInvalidLock() { - try { - const userDataPath = app.getPath('userData') - const possibleLockFiles = [ - path.join(userDataPath, 'single-instance-lock'), - path.join(userDataPath, 'lockfile'), - path.join(userDataPath, '.lock'), - path.join(process.cwd(), 'single-instance-lock'), - path.join(process.cwd(), 'lockfile'), - path.join(process.cwd(), '.lock') - ] - - for (const lockFile of possibleLockFiles) { - if (fs.existsSync(lockFile)) { - logger.info(`发现锁文件: ${lockFile}`) - - // 检查锁文件是否有效(检查进程是否还在运行) - try { - const lockContent = fs.readFileSync(lockFile, 'utf8') - const pid = parseInt(lockContent.trim()) - - if (pid && this.isProcessRunning(pid)) { - logger.info(`进程 ${pid} 仍在运行,锁文件有效`) - } else { - logger.info(`进程 ${pid} 不存在,尝试删除无效锁文件`) - this.safeDeleteFile(lockFile) - } - } catch (error) { - logger.info(`锁文件无效,尝试删除: ${lockFile}`) - this.safeDeleteFile(lockFile) - } - } - } - } catch (error) { - logger.error('清理无效锁文件失败:', error) - } - } - - /** - * 安全删除文件,处理文件被占用的情况 - */ - safeDeleteFile(filePath) { - try { - // 首先尝试直接删除 - fs.unlinkSync(filePath) - logger.info(`成功删除文件: ${filePath}`) - } catch (error) { - if (error.code === 'EBUSY' || error.code === 'EACCES') { - logger.warn(`文件被占用,无法删除: ${filePath}`) - logger.warn(`错误信息: ${error.message}`) - - // 在 Windows 上尝试使用 del 命令 - if (process.platform === 'win32') { - try { - const { execSync } = require('child_process') - execSync(`del /F /Q "${filePath}"`, { stdio: 'ignore' }) - logger.info(`使用 del 命令删除文件: ${filePath}`) - } catch (delError) { - logger.error(`del 命令删除失败: ${filePath}`, delError) - } - } - } else { - logger.error(`删除文件失败: ${filePath}`, error) - } - } - } - - /** - * 检查进程是否还在运行 - */ - isProcessRunning(pid) { - try { - // 在 Windows 上使用 tasklist 命令检查进程 - if (process.platform === 'win32') { - const { execSync } = require('child_process') - const result = execSync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`, { encoding: 'utf8' }) - return result.includes(pid.toString()) - } else { - // 在 Unix 系统上使用 kill -0 检查进程 - const { execSync } = require('child_process') - execSync(`kill -0 ${pid}`, { stdio: 'ignore' }) - return true - } - } catch (error) { + checkSingleInstance() { + // 直接使用 Electron 的单实例锁 + const gotTheLock = app.requestSingleInstanceLock() + logger.info(`单实例锁检查结果: ${gotTheLock}`) + + if (gotTheLock) { + // 成功获取锁,创建锁文件作为备份 + this.createLock() + return true + } else { + // 无法获取锁,说明已有实例运行 + logger.info('检测到已有实例运行,阻止新实例启动') return false } } /** - * 创建锁文件 + * 创建锁文件作为备份 */ createLock() { try { @@ -162,44 +92,6 @@ export class SingleInstanceManager { } } } - - /** - * 检查是否为第一个实例 - */ - checkSingleInstance() { - // 首先清理可能存在的无效锁 - this.cleanupInvalidLock() - - // 尝试获取单实例锁 - const gotTheLock = app.requestSingleInstanceLock() - logger.info(`单实例锁检查结果: ${gotTheLock}`) - - if (gotTheLock) { - // 成功获取锁,创建锁文件 - this.createLock() - return true - } else { - // 无法获取锁,检查是否真的有必要阻止启动 - logger.info('检测到已有实例运行') - - // 检查是否有实际的窗口存在 - const windows = require('electron').BrowserWindow.getAllWindows() - if (windows.length === 0) { - logger.warn('未找到现有实例窗口,可能是锁文件问题,允许强制启动') - // 尝试强制获取锁 - try { - this.createLock() - return true - } catch (error) { - logger.error('强制创建锁失败:', error) - return false - } - } else { - logger.info(`找到 ${windows.length} 个现有窗口,阻止新实例启动`) - return false - } - } - } } export default SingleInstanceManager \ No newline at end of file diff --git a/test-exit-function.js b/test-exit-function.js new file mode 100644 index 0000000..4bd034b --- /dev/null +++ b/test-exit-function.js @@ -0,0 +1,154 @@ +const { app, BrowserWindow, Tray, Menu } = require('electron') +const path = require('path') + +console.log('=== 退出功能测试 ===') + +let mainWindow = null +let tray = null + +// 单实例检查 +const gotTheLock = app.requestSingleInstanceLock() + +if (!gotTheLock) { + console.log('❌ 检测到已有实例运行,退出当前实例') + app.quit() +} else { + console.log('✅ 这是第一个实例,继续启动') + + // 监听第二个实例 + app.on('second-instance', (event, commandLine, workingDirectory) => { + console.log('检测到第二个实例启动,激活现有实例') + if (mainWindow) { + if (mainWindow.isMinimized()) { + mainWindow.restore() + } + mainWindow.show() + mainWindow.focus() + } + }) + + // 创建窗口 + function createWindow() { + mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + nodeIntegration: true, + contextIsolation: false + } + }) + + mainWindow.loadURL('data:text/html,

退出功能测试

PID: ' + process.pid + '

时间: ' + new Date().toLocaleString() + '

点击托盘图标右键菜单中的"退出应用"来测试退出功能

') + + console.log('窗口已创建,PID:', process.pid) + + mainWindow.on('closed', () => { + mainWindow = null + }) + } + + // 创建托盘 + function createTray() { + tray = new Tray(path.join(__dirname, 'resources', 'icon.png')) + + const contextMenu = Menu.buildFromTemplate([ + { + label: '显示窗口', + click: () => { + if (mainWindow) { + mainWindow.show() + } + } + }, + { + label: '隐藏窗口', + click: () => { + if (mainWindow) { + mainWindow.hide() + } + } + }, + { type: 'separator' }, + { + label: '退出应用', + click: () => { + console.log('用户点击退出应用') + + // 确保所有窗口都被关闭 + const windows = require('electron').BrowserWindow.getAllWindows() + console.log(`准备关闭 ${windows.length} 个窗口`) + + windows.forEach(window => { + if (!window.isDestroyed()) { + try { + window.destroy() + console.log('窗口销毁成功') + } catch (error) { + console.log('销毁窗口时出错:', error) + } + } + }) + + // 延迟执行退出,确保窗口销毁完成 + setTimeout(() => { + try { + console.log('执行应用退出') + // 使用 exit 而不是 quit,确保完全退出 + app.exit(0) + } catch (error) { + console.log('应用退出失败,尝试强制退出:', error) + try { + process.exit(0) + } catch (processError) { + console.log('强制退出也失败:', processError) + // 最后的强制退出 + process.kill(process.pid, 'SIGKILL') + } + } + }, 500) + } + } + ]) + + tray.setContextMenu(contextMenu) + tray.setToolTip('退出功能测试') + + // 点击托盘图标 + tray.on('click', () => { + if (mainWindow) { + if (mainWindow.isVisible()) { + mainWindow.hide() + } else { + mainWindow.show() + } + } + }) + } + + app.whenReady().then(() => { + console.log('应用已准备就绪') + createWindow() + createTray() + }) + + app.on('window-all-closed', () => { + console.log('所有窗口已关闭') + if (process.platform !== 'darwin') { + app.quit() + } + }) + + app.on('before-quit', () => { + console.log('应用即将退出') + }) + + app.on('will-quit', () => { + console.log('应用即将退出') + }) + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } + }) +} \ No newline at end of file diff --git a/test-single-instance-simple.js b/test-single-instance-simple.js new file mode 100644 index 0000000..3baf5ef --- /dev/null +++ b/test-single-instance-simple.js @@ -0,0 +1,66 @@ +const { app, BrowserWindow } = require('electron') +const path = require('path') + +console.log('=== 简单单实例测试 ===') + +// 单实例检查 +const gotTheLock = app.requestSingleInstanceLock() + +if (!gotTheLock) { + console.log('❌ 检测到已有实例运行,退出当前实例') + app.quit() +} else { + console.log('✅ 这是第一个实例,继续启动') + + let mainWindow = null + + // 监听第二个实例 + app.on('second-instance', (event, commandLine, workingDirectory) => { + console.log('检测到第二个实例启动,激活现有实例') + if (mainWindow) { + if (mainWindow.isMinimized()) { + mainWindow.restore() + } + mainWindow.show() + mainWindow.focus() + } + }) + + // 创建窗口 + function createWindow() { + mainWindow = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + nodeIntegration: true, + contextIsolation: false + } + }) + + mainWindow.loadURL('data:text/html,

单实例测试

PID: ' + process.pid + '

时间: ' + new Date().toLocaleString() + '

') + + console.log('窗口已创建,PID:', process.pid) + + mainWindow.on('closed', () => { + mainWindow = null + }) + } + + app.whenReady().then(() => { + console.log('应用已准备就绪') + createWindow() + }) + + app.on('window-all-closed', () => { + console.log('所有窗口已关闭') + if (process.platform !== 'darwin') { + app.quit() + } + }) + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow() + } + }) +} \ No newline at end of file