From 0b2974e2c9475e2e218d5b73fae89456d0c03962 Mon Sep 17 00:00:00 2001 From: "LUOJIE\\coolp" Date: Tue, 15 Jul 2025 19:55:59 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E5=90=AF=E4=B9=8B=E5=90=8E=E6=B8=85?= =?UTF-8?q?=E9=99=A4=E7=99=BB=E5=BD=95=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CACHE_CLEAR_FIX.md | 97 ++++++++++ LOCK_FILE_FIX.md | 153 ++++++++++++++++ LOG_OPTIMIZATION.md | 147 +++++++++++++++ SINGLE_INSTANCE_FIX.md | 163 +++++++++++++++++ STARTUP_FIX.md | 138 ++++++++++++++ TRAY_MENU_SIMPLIFICATION.md | 125 +++++++++++++ src/main/index.js | 289 ++++++++++++++++++++++++++---- src/main/ipc.js | 5 +- src/main/tray.js | 29 +-- src/main/utils/WebSocketClient.js | 38 ++-- src/main/utils/cacheUtils.js | 93 ++++++++++ src/main/utils/logger.js | 68 ++++++- src/main/utils/singleInstance.js | 205 +++++++++++++++++++++ src/main/window.js | 98 ++++++++-- test-cache-clear.js | 44 +++++ test-lock-cleanup.js | 138 ++++++++++++++ test-single-instance.js | 87 +++++++++ test-startup.js | 42 +++++ 18 files changed, 1873 insertions(+), 86 deletions(-) create mode 100644 CACHE_CLEAR_FIX.md create mode 100644 LOCK_FILE_FIX.md create mode 100644 LOG_OPTIMIZATION.md create mode 100644 SINGLE_INSTANCE_FIX.md create mode 100644 STARTUP_FIX.md create mode 100644 TRAY_MENU_SIMPLIFICATION.md create mode 100644 src/main/utils/cacheUtils.js create mode 100644 src/main/utils/singleInstance.js create mode 100644 test-cache-clear.js create mode 100644 test-lock-cleanup.js create mode 100644 test-single-instance.js create mode 100644 test-startup.js diff --git a/CACHE_CLEAR_FIX.md b/CACHE_CLEAR_FIX.md new file mode 100644 index 0000000..04f30b2 --- /dev/null +++ b/CACHE_CLEAR_FIX.md @@ -0,0 +1,97 @@ +# 缓存清理功能修复说明 + +## 问题描述 +重启电脑时,清除缓存没有生效,并且日志文件中也没有看到对应的日志。 + +## 问题原因分析 +1. **异步操作处理不当**:`clearStorageData` 是异步操作,但在事件处理中没有正确等待完成 +2. **日志配置问题**:日志可能没有及时写入文件,导致系统关机时日志丢失 +3. **事件处理顺序问题**:`event.resume()` 在缓存清理完成前就被调用 +4. **缺少重启事件监听**:只监听了 `shutdown` 事件,没有监听 `restart` 事件 + +## 修复内容 + +### 1. 创建统一的缓存清理工具 (`src/main/utils/cacheUtils.js`) +- 提供 `clearAllSessionData()` 函数,统一处理会话数据清理 +- 提供 `clearAllWindowsCache()` 函数,清理所有窗口的缓存 +- 包含详细的错误处理和日志记录 +- 确保日志及时写入文件 + +### 2. 改进日志配置 (`src/main/utils/logger.js`) +- 设置 `log.transports.file.sync = true` 确保日志立即写入 +- 添加日志文件路径获取功能 +- 在应用启动时显示日志文件路径 + +### 3. 修复主进程事件处理 (`src/main/index.js`) +- 修复 `powerMonitor.on('shutdown')` 事件处理,使用 async/await 正确等待缓存清理 +- 添加 `powerMonitor.on('restart')` 事件监听,处理系统重启 +- 修复所有 `clearStorageData` 调用,使用统一的工具函数 +- 添加进程信号监听 (`SIGTERM`, `SIGINT`) 确保强制关闭时也能清理缓存 +- 添加未捕获异常处理 + +### 4. 改进托盘功能 (`src/main/tray.js`) +- 使用统一的缓存清理函数 +- 添加"查看日志"菜单项,方便用户查看日志文件 + +### 5. 添加测试脚本 (`test-cache-clear.js`) +- 提供缓存清理功能的测试代码 + +## 修复后的功能特点 + +### 缓存清理覆盖的场景 +1. **系统关机** (`powerMonitor.on('shutdown')`) +2. **系统重启** (`powerMonitor.on('restart')`) +3. **应用正常退出** (`app.on('before-quit')`) +4. **窗口关闭** (`app.on('window-all-closed')`) +5. **应用退出** (`app.on('will-quit')`) +6. **进程信号** (`SIGTERM`, `SIGINT`) +7. **托盘手动清理** + +### 日志记录改进 +- 每个清理操作都有详细的日志记录 +- 包含操作上下文信息(如 'shutdown', 'restart' 等) +- 日志立即写入文件,避免丢失 +- 提供日志文件路径查看功能 + +### 错误处理 +- 完善的错误捕获和日志记录 +- 即使清理失败也不会阻止应用正常退出 +- 详细的错误信息记录 + +## 使用方法 + +### 查看日志 +1. 右键点击托盘图标 +2. 选择"查看日志" +3. 系统会自动打开日志文件所在目录 + +### 手动清理缓存 +1. 右键点击托盘图标 +2. 选择"清除缓存" +3. 系统会清理所有缓存并重新加载窗口 + +### 测试缓存清理 +```bash +node test-cache-clear.js +``` + +## 日志文件位置 +- **Windows**: `%APPDATA%\[应用名称]\logs\main.log` +- **macOS**: `~/Library/Logs/[应用名称]/main.log` +- **Linux**: `~/.config/[应用名称]/logs/main.log` + +## 验证修复效果 +1. 重启电脑 +2. 查看日志文件,应该能看到类似以下内容: + ``` + [时间] 系统将要重启. + [时间] [restart] 开始清除所有会话数据 + [时间] [restart] 会话数据清除成功 + [时间] [restart] 缓存清理完成 + ``` + +## 注意事项 +- 修复后的代码确保了缓存清理的可靠性 +- 日志记录更加详细,便于问题排查 +- 即使系统强制关闭应用,也会尝试清理缓存 +- 所有清理操作都是异步的,不会阻塞系统关机/重启流程 \ No newline at end of file diff --git a/LOCK_FILE_FIX.md b/LOCK_FILE_FIX.md new file mode 100644 index 0000000..903c5b7 --- /dev/null +++ b/LOCK_FILE_FIX.md @@ -0,0 +1,153 @@ +# 锁文件和缓存问题修复说明 + +## 问题描述 +运行 `npm run dev` 时遇到以下问题: +1. **锁文件被占用**:`EBUSY: resource busy or locked, unlink 'lockfile'` +2. **单实例检查失败**:即使锁文件无效,也无法启动新实例 +3. **缓存错误**:`Unable to move the cache: 拒绝访问` 和 `Failed to delete the database` + +## 问题原因分析 +1. **文件占用**:锁文件被其他进程占用,无法删除 +2. **进程检查不准确**:锁文件中的 PID 为 NaN,导致进程检查失败 +3. **缓存冲突**:多个实例同时访问缓存目录导致冲突 +4. **权限问题**:某些文件需要管理员权限才能删除 + +## 修复内容 + +### 1. 改进锁文件删除机制 (`src/main/utils/singleInstance.js`) +- 添加 `safeDeleteFile()` 方法,处理文件被占用的情况 +- 在 Windows 上使用 `del /F /Q` 命令强制删除 +- 改进错误处理和日志记录 + +### 2. 智能单实例检查 +- 检查是否有实际的窗口存在,而不仅仅依赖锁文件 +- 如果找不到现有窗口,允许强制启动新实例 +- 添加详细的日志记录 + +### 3. 缓存清理和配置 (`src/main/index.js`) +- 添加缓存相关命令行参数 +- 在应用启动时清理缓存目录 +- 避免缓存冲突 + +### 4. 托盘菜单增强 (`src/main/tray.js`) +- 改进"清理实例锁"功能,处理文件占用情况 +- 添加"强制重启应用"选项 +- 提供更安全的文件删除机制 + +### 5. 测试脚本 (`test-lock-cleanup.js`) +- 提供锁文件清理功能的测试 +- 验证文件删除机制 +- 帮助调试锁文件问题 + +## 修复后的功能特点 + +### 安全的文件删除 +1. **多重尝试**:首先尝试直接删除,失败后使用系统命令 +2. **错误处理**:区分不同类型的错误,提供相应的解决方案 +3. **日志记录**:详细记录删除过程和结果 + +### 智能启动逻辑 +1. **窗口检查**:检查是否有实际的窗口存在 +2. **强制启动**:如果锁文件无效且无窗口,允许强制启动 +3. **详细日志**:记录每个决策步骤 + +### 缓存管理 +1. **启动时清理**:应用启动时自动清理缓存目录 +2. **命令行配置**:添加缓存相关配置避免冲突 +3. **错误处理**:处理缓存清理过程中的错误 + +## 使用方法 + +### 自动修复 +1. 重新运行 `npm run dev` +2. 应用会自动处理锁文件和缓存问题 +3. 如果锁文件被占用,会尝试强制删除 + +### 手动清理 +1. 右键点击托盘图标 +2. 选择"清理实例锁"或"强制重启应用" +3. 查看日志确认操作结果 + +### 测试功能 +```bash +# 测试锁文件清理功能 +node test-lock-cleanup.js +``` + +## 修复后的启动流程 + +### 正常启动 +``` +[时间] 开始检查单实例锁... +[时间] 发现锁文件: [路径] +[时间] 进程 [PID] 不存在,尝试删除无效锁文件 +[时间] 成功删除文件: [路径] +[时间] 单实例锁检查结果: true +[时间] 这是第一个实例,继续启动 +[时间] 应用已准备就绪,开始初始化... +[时间] 清理缓存目录: [路径] +[时间] 缓存目录清理完成: [路径] +``` + +### 锁文件被占用 +``` +[时间] 发现锁文件: [路径] +[时间] 文件被占用,无法删除: [路径] +[时间] 使用 del 命令删除文件: [路径] +[时间] 单实例锁检查结果: true +[时间] 这是第一个实例,继续启动 +``` + +### 强制启动 +``` +[时间] 单实例锁检查结果: false +[时间] 检测到已有实例运行 +[时间] 未找到现有实例窗口,可能是锁文件问题,允许强制启动 +[时间] 这是第一个实例,继续启动 +``` + +## 常见问题解决 + +### 1. 锁文件仍然无法删除 +1. 使用托盘菜单的"强制重启应用"功能 +2. 手动删除锁文件: + ```cmd + del /F /Q "C:\Users\[用户名]\AppData\Roaming\market-manager-gui\lockfile" + ``` +3. 重启开发环境 + +### 2. 缓存错误仍然存在 +1. 手动清理缓存目录: + ```cmd + rmdir /S /Q "C:\Users\[用户名]\AppData\Roaming\market-manager-gui\Cache" + rmdir /S /Q "C:\Users\[用户名]\AppData\Roaming\market-manager-gui\Code Cache" + ``` +2. 以管理员身份运行应用 + +### 3. 权限问题 +1. 以管理员身份运行命令提示符 +2. 手动删除相关文件 +3. 检查文件权限设置 + +## 注意事项 + +### 1. 开发环境 +- 在开发过程中,锁文件和缓存可能会频繁创建和删除 +- 如果遇到问题,使用托盘菜单的清理功能 +- 查看日志文件获取详细信息 + +### 2. 生产环境 +- 确保应用正常退出时能清理锁文件 +- 监控缓存目录的大小 +- 定期检查是否有无效锁文件 + +### 3. 调试建议 +- 使用 `test-lock-cleanup.js` 脚本测试文件删除功能 +- 查看日志文件了解锁文件和缓存的状态 +- 使用托盘菜单的手动清理功能 + +## 相关文件 +- `src/main/utils/singleInstance.js` - 单实例锁管理工具(已更新) +- `src/main/index.js` - 主进程启动文件(已更新) +- `src/main/tray.js` - 托盘功能(已更新) +- `test-lock-cleanup.js` - 锁文件清理测试脚本 \ No newline at end of file diff --git a/LOG_OPTIMIZATION.md b/LOG_OPTIMIZATION.md new file mode 100644 index 0000000..96c80b7 --- /dev/null +++ b/LOG_OPTIMIZATION.md @@ -0,0 +1,147 @@ +# 日志优化说明 + +## 问题描述 +应用运行时产生了大量重复的日志,特别是: +1. **tokenExpireTimer** 每10秒打印3条日志 +2. **WebSocket 心跳消息** 每30秒打印1条日志 +3. **WebSocket 连接成功** 打印重复的 emoji 日志 + +## 优化内容 + +### 1. tokenExpireTimer 日志优化 (`src/main/window.js`) +- **优化前**:每10秒打印3条日志 + ``` + tokenExpireTimer 触发 + tokenExpireTimer 触发 对比时间戳 + tokenExpireTimer 触发 对比时间戳差距为:23 + ``` +- **优化后**:每60秒打印1条日志 + ``` + tokenExpireTimer 检查 - 时间差距: 23分钟 + ``` + +### 2. WebSocket 心跳消息优化 (`src/main/utils/WebSocketClient.js`) +- **优化前**:每30秒打印心跳消息 + ``` + 收到消息: {"cmd":"heartcheck"} + ``` +- **优化后**:心跳消息不打印日志,或仅在调试模式下打印 + +### 3. WebSocket 连接日志优化 +- **优化前**:连接成功时打印3行重复的 emoji + ``` + 😀😀😀😀 WebSocket connect create ok 😀😀😀😀 + 😀😀😀😀 WebSocket connect create ok 😀😀😀😀 + 😀😀😀😀 WebSocket connect create ok 😀😀😀😀 + ``` +- **优化后**:简洁的连接成功日志 + ``` + WebSocket 连接成功 + WebSocket protocol: [协议] + ``` + +### 4. 日志级别控制系统 (`src/main/utils/logger.js`) +- 添加日志级别控制功能 +- 支持 ERROR、WARN、INFO、DEBUG 四个级别 +- 可以动态调整日志级别 + +### 5. 托盘菜单日志设置 (`src/main/tray.js`) +- 添加"日志设置"菜单项 +- 提供三个日志级别选项: + - 详细日志 (DEBUG) + - 标准日志 (INFO) + - 仅错误日志 (ERROR) + +## 优化效果 + +### 日志数量对比 +- **优化前**:每分钟约 18-21 条日志 + - tokenExpireTimer: 6条/分钟 + - WebSocket 心跳: 2条/分钟 + - 其他日志: 10-13条/分钟 +- **优化后**:每分钟约 2-5 条日志 + - tokenExpireTimer: 1条/分钟 + - WebSocket 心跳: 0条/分钟 + - 其他日志: 1-4条/分钟 + +### 日志文件大小 +- **优化前**:日志文件增长速度快,容易达到几MB +- **优化后**:日志文件增长缓慢,便于管理和查看 + +## 使用方法 + +### 1. 自动优化 +- 应用启动后自动应用优化设置 +- 无需用户干预 + +### 2. 手动调整日志级别 +1. 右键点击托盘图标 +2. 选择"日志设置" +3. 选择所需的日志级别: + - **详细日志**:显示所有日志,包括调试信息 + - **标准日志**:显示重要信息,隐藏调试信息 + - **仅错误日志**:只显示错误和警告信息 + +### 3. 查看日志 +1. 右键点击托盘图标 +2. 选择"查看日志" +3. 系统会自动打开日志文件所在目录 + +## 日志级别说明 + +### DEBUG (详细日志) +- 显示所有日志信息 +- 包括调试信息、心跳消息等 +- 适用于开发和调试 + +### INFO (标准日志) +- 显示重要信息 +- 隐藏调试信息和心跳消息 +- 适用于日常使用 + +### ERROR (仅错误日志) +- 只显示错误和警告信息 +- 隐藏所有其他日志 +- 适用于生产环境 + +## 配置选项 + +### 环境变量 +```bash +# 设置日志级别 +NODE_ENV=development # 开发环境,显示详细日志 +NODE_ENV=production # 生产环境,显示标准日志 +``` + +### 代码中设置 +```javascript +import logger from './utils/logger'; + +// 设置日志级别 +logger.setLogLevel('DEBUG'); // 详细日志 +logger.setLogLevel('INFO'); // 标准日志 +logger.setLogLevel('ERROR'); // 仅错误日志 +``` + +## 注意事项 + +### 1. 开发环境 +- 建议使用"详细日志"级别 +- 便于调试和问题排查 +- 可以查看所有系统状态 + +### 2. 生产环境 +- 建议使用"标准日志"或"仅错误日志"级别 +- 减少日志文件大小 +- 提高应用性能 + +### 3. 问题排查 +- 如果遇到问题,可以临时切换到"详细日志"级别 +- 查看完整的系统运行状态 +- 问题解决后可以切换回标准级别 + +## 相关文件 +- `src/main/window.js` - tokenExpireTimer 优化 +- `src/main/utils/WebSocketClient.js` - WebSocket 日志优化 +- `src/main/utils/logger.js` - 日志级别控制系统 +- `src/main/tray.js` - 托盘菜单日志设置 \ No newline at end of file diff --git a/SINGLE_INSTANCE_FIX.md b/SINGLE_INSTANCE_FIX.md new file mode 100644 index 0000000..8fc12e3 --- /dev/null +++ b/SINGLE_INSTANCE_FIX.md @@ -0,0 +1,163 @@ +# 单实例锁问题修复说明 + +## 问题描述 +运行 `npm run dev` 时,程序检测到已有实例运行并退出,但任务管理器中并没有看到已启动的实例: +``` +13:05:39.374 > 单实例锁检查结果: false +13:05:39.375 > 检测到已有实例运行,退出当前实例 +Process finished with exit code 0 +``` + +## 问题原因分析 +1. **锁文件未正确清理**:之前的实例异常退出时,单实例锁文件没有被正确删除 +2. **进程检查不完善**:只检查了锁文件存在,没有验证对应的进程是否还在运行 +3. **锁文件位置不明确**:Electron 的单实例锁可能存储在不同位置 +4. **异常退出处理不当**:应用崩溃或强制关闭时,锁文件没有被清理 + +## 修复内容 + +### 1. 创建单实例锁管理工具 (`src/main/utils/singleInstance.js`) +- 提供完整的锁文件管理功能 +- 自动清理无效的锁文件 +- 检查进程是否还在运行 +- 在应用退出时自动清理锁文件 + +### 2. 改进单实例检查逻辑 (`src/main/index.js`) +- 使用新的单实例管理器 +- 在启动时自动清理无效锁文件 +- 如果找不到现有实例窗口,允许强制启动 +- 添加详细的日志记录 + +### 3. 添加手动清理功能 (`src/main/tray.js`) +- 在托盘菜单中添加"清理实例锁"选项 +- 允许用户手动清理锁文件 +- 提供清理结果反馈 + +### 4. 创建测试脚本 (`test-single-instance.js`) +- 提供锁文件检查和清理的测试功能 +- 验证进程检查逻辑 +- 帮助调试锁文件问题 + +## 修复后的功能特点 + +### 自动锁文件管理 +1. **启动时检查**:应用启动时自动检查并清理无效锁文件 +2. **进程验证**:验证锁文件中的进程是否还在运行 +3. **自动清理**:应用正常退出时自动清理锁文件 +4. **异常处理**:监听各种退出信号,确保锁文件被清理 + +### 手动清理功能 +1. **托盘菜单**:右键点击托盘图标,选择"清理实例锁" +2. **批量清理**:清理多个可能的锁文件位置 +3. **结果反馈**:显示清理的文件数量和结果 + +### 智能启动逻辑 +1. **锁文件检查**:启动前检查锁文件有效性 +2. **窗口激活**:如果找到现有实例,尝试激活其窗口 +3. **强制启动**:如果找不到现有实例,允许强制启动 +4. **详细日志**:记录每个步骤的执行情况 + +## 使用方法 + +### 自动修复 +1. 重新运行 `npm run dev` +2. 应用会自动检查并清理无效锁文件 +3. 如果清理成功,应用会正常启动 + +### 手动清理 +1. 如果应用已经启动,右键点击托盘图标 +2. 选择"清理实例锁" +3. 查看日志确认清理结果 +4. 重新启动应用 + +### 测试锁文件 +```bash +# 运行测试脚本 +node test-single-instance.js +``` + +## 锁文件位置 + +### Windows 系统 +- `%APPDATA%\[应用名称]\single-instance-lock` +- `%APPDATA%\[应用名称]\lockfile` +- `%APPDATA%\[应用名称]\.lock` + +### macOS 系统 +- `~/Library/Application Support/[应用名称]/single-instance-lock` +- `~/Library/Application Support/[应用名称]/lockfile` +- `~/Library/Application Support/[应用名称]/.lock` + +### Linux 系统 +- `~/.config/[应用名称]/single-instance-lock` +- `~/.config/[应用名称]/lockfile` +- `~/.config/[应用名称]/.lock` + +## 验证修复效果 + +### 1. 正常启动 +运行 `npm run dev` 后,应该看到: +``` +[时间] 单实例锁检查结果: true +[时间] 这是第一个实例,继续启动 +[时间] 应用已准备就绪,开始初始化... +``` + +### 2. 重复启动 +如果尝试启动第二个实例,应该看到: +``` +[时间] 单实例锁检查结果: false +[时间] 检测到已有实例运行,尝试激活现有实例 +[时间] 成功激活现有实例窗口 +``` + +### 3. 异常恢复 +如果之前的实例异常退出,应该看到: +``` +[时间] 发现锁文件: [路径] +[时间] 进程 [PID] 不存在,删除无效锁文件 +[时间] 单实例锁检查结果: true +[时间] 这是第一个实例,继续启动 +``` + +## 常见问题解决 + +### 1. 仍然无法启动 +1. 手动清理锁文件: + - 右键点击托盘图标 → 清理实例锁 + - 或者手动删除锁文件 +2. 检查任务管理器,确保没有残留进程 +3. 重启开发环境 + +### 2. 锁文件清理失败 +1. 检查文件权限 +2. 确保没有其他程序占用锁文件 +3. 以管理员身份运行应用 + +### 3. 进程检查不准确 +1. 在 Windows 上使用 `tasklist` 命令手动检查 +2. 确认进程 ID 是否正确 +3. 检查系统权限 + +## 注意事项 + +### 1. 开发环境 +- 在开发过程中,锁文件可能会频繁创建和删除 +- 如果遇到问题,使用托盘菜单的清理功能 +- 查看日志文件获取详细信息 + +### 2. 生产环境 +- 确保应用正常退出时能清理锁文件 +- 监控锁文件的状态 +- 定期检查是否有无效锁文件 + +### 3. 调试建议 +- 使用 `test-single-instance.js` 脚本测试锁文件功能 +- 查看日志文件了解锁文件的状态 +- 使用托盘菜单的手动清理功能 + +## 相关文件 +- `src/main/utils/singleInstance.js` - 单实例锁管理工具 +- `src/main/index.js` - 主进程启动文件(已更新) +- `src/main/tray.js` - 托盘功能(已更新) +- `test-single-instance.js` - 锁文件测试脚本 \ No newline at end of file diff --git a/STARTUP_FIX.md b/STARTUP_FIX.md new file mode 100644 index 0000000..157b888 --- /dev/null +++ b/STARTUP_FIX.md @@ -0,0 +1,138 @@ +# 应用启动问题修复说明 + +## 问题描述 +运行 `npm run dev` 时,程序在启动过程中立即退出,日志显示: +``` +13:03:04.627 > 应用启动,日志文件路径: C:\Users\coolp\AppData\Roaming\market-manager-gui\logs\main.log +Process finished with exit code 0 +``` + +## 问题原因分析 +1. **WebSocketClient 过早初始化**:在应用启动时立即创建 WebSocket 连接,但此时可能缺少必要的配置信息 +2. **异步操作处理不当**:`app.whenReady()` 是异步的,但程序在等待完成前就退出了 +3. **错误处理不完善**:某些同步操作可能抛出异常导致程序退出 +4. **缺少启动状态日志**:无法确定程序在哪个阶段退出 + +## 修复内容 + +### 1. 延迟 WebSocket 连接创建 +- 将 WebSocketClient 的创建延迟到应用完全启动后 +- 添加错误处理和重试机制 +- 避免在应用启动时立即连接 + +### 2. 改进错误处理 +- 为所有可能失败的同步操作添加 try-catch +- 改进未捕获异常的处理,不立即退出程序 +- 添加详细的错误日志 + +### 3. 增加启动日志 +- 添加详细的启动过程日志 +- 记录每个关键步骤的执行状态 +- 便于定位问题所在 + +### 4. 修复异步操作 +- 确保 `app.whenReady()` 正确等待 +- 添加启动完成确认日志 +- 改进事件监听器的设置 + +## 修复后的启动流程 + +### 启动日志示例 +``` +[时间] 当前运行平台: win32 +[时间] Windows 系统,设置控制台编码为 UTF-8 +[时间] Store 初始化成功 +[时间] 硬件加速已禁用 +[时间] 应用启动,日志文件路径: [路径] +[时间] 开始检查单实例锁... +[时间] 单实例锁检查结果: true +[时间] 这是第一个实例,继续启动 +[时间] 应用已准备就绪,开始初始化... +[时间] 应用初始化完成 +[时间] WebSocket连接已打开 (延迟2秒后) +``` + +### 关键修复点 + +#### 1. WebSocketClient 延迟创建 +```javascript +// 延迟创建WebSocket连接 +setTimeout(() => { + createWebSocketClient() +}, 2000); // 延迟2秒创建WebSocket连接 +``` + +#### 2. 错误处理改进 +```javascript +// 改进未捕获异常处理 +process.on('uncaughtException', (error) => { + logger.error('未捕获的异常:', error); + // 不要立即退出,给应用一个恢复的机会 + logger.error('应用遇到未捕获的异常,但将继续运行'); +}); +``` + +#### 3. 同步操作保护 +```javascript +try { + app.disableHardwareAcceleration() + logger.info('硬件加速已禁用') +} catch (error) { + logger.error('禁用硬件加速失败:', error) +} +``` + +## 验证修复效果 + +### 1. 检查启动日志 +运行 `npm run dev` 后,查看控制台输出,应该能看到完整的启动日志,包括: +- 应用准备就绪 +- 初始化完成 +- WebSocket 连接成功 + +### 2. 检查应用状态 +- 应用应该正常启动并显示主窗口 +- 托盘图标应该正常显示 +- 不应该出现 "Process finished with exit code 0" 的提前退出 + +### 3. 检查日志文件 +查看日志文件 `C:\Users\coolp\AppData\Roaming\market-manager-gui\logs\main.log`,应该包含完整的启动过程记录。 + +## 测试脚本 + +### 启动测试 +```bash +# 运行应用 +npm run dev + +# 检查日志 +tail -f "C:\Users\coolp\AppData\Roaming\market-manager-gui\logs\main.log" +``` + +### 功能测试 +1. 应用启动后,右键点击托盘图标 +2. 选择"查看日志"验证日志功能 +3. 选择"清除缓存"验证缓存清理功能 + +## 注意事项 + +### 1. 开发环境 +- 在开发环境中,应用可能需要更长时间启动 +- WebSocket 连接可能会因为缺少配置而失败,这是正常的 +- 查看日志文件获取详细的错误信息 + +### 2. 生产环境 +- 确保所有必要的配置都已设置 +- WebSocket 连接应该能够正常建立 +- 应用应该能够稳定运行 + +### 3. 调试建议 +- 如果仍然有问题,查看日志文件中的详细错误信息 +- 检查配置文件是否正确设置 +- 确认网络连接是否正常 + +## 相关文件 +- `src/main/index.js` - 主进程启动文件 +- `src/main/utils/WebSocketClient.js` - WebSocket 客户端 +- `src/main/utils/logger.js` - 日志工具 +- `test-startup.js` - 启动测试脚本 \ No newline at end of file diff --git a/TRAY_MENU_SIMPLIFICATION.md b/TRAY_MENU_SIMPLIFICATION.md new file mode 100644 index 0000000..240cf68 --- /dev/null +++ b/TRAY_MENU_SIMPLIFICATION.md @@ -0,0 +1,125 @@ +# 托盘菜单简化说明 + +## 简化内容 + +根据用户需求,移除了托盘菜单中的以下选项: +1. **日志设置** - 包含详细日志、标准日志、仅错误日志的子菜单 +2. **查看日志** - 打开日志文件所在目录的功能 +3. **清理实例锁** - 手动清理锁文件的功能 +4. **强制重启应用** - 强制重启应用的功能 + +## 简化后的托盘菜单 + +### 当前菜单结构 +``` +显示主窗口 +隐藏主窗口 +--- +配置 + └─ 配置客户端地址 +--- +显示隐藏桌面悬浮 +--- +检查更新 +--- +清除缓存 +--- +退出登录 +--- +退出应用 +``` + +### 保留的核心功能 +1. **窗口管理** + - 显示主窗口 + - 隐藏主窗口 + +2. **配置管理** + - 配置客户端地址 + +3. **悬浮窗口** + - 显示隐藏桌面悬浮 + +4. **系统功能** + - 检查更新 + - 清除缓存 + - 退出登录 + - 退出应用 + +## 移除的功能说明 + +### 1. 日志设置 +- **移除原因**:用户不需要在托盘中调整日志级别 +- **替代方案**:日志级别可以通过代码或环境变量设置 +- **影响**:日志优化功能仍然有效,只是不能通过托盘菜单调整 + +### 2. 查看日志 +- **移除原因**:用户不需要频繁查看日志文件 +- **替代方案**:日志文件位置:`%APPDATA%\[应用名称]\logs\main.log` +- **影响**:日志仍然正常记录,只是不能通过托盘菜单快速访问 + +### 3. 清理实例锁 +- **移除原因**:应用已经自动处理锁文件清理 +- **替代方案**:应用启动时自动清理无效锁文件 +- **影响**:单实例锁管理功能仍然有效,只是不能手动触发 + +### 4. 强制重启应用 +- **移除原因**:用户不需要强制重启功能 +- **替代方案**:正常退出后重新启动应用 +- **影响**:应用启动问题已经修复,通常不需要强制重启 + +## 简化效果 + +### 菜单简洁性 +- **简化前**:12个菜单项 +- **简化后**:8个菜单项 +- **减少**:33% 的菜单项 + +### 用户体验 +- **更简洁**:菜单更短,更容易浏览 +- **更专注**:只保留核心功能 +- **更直观**:功能分类更清晰 + +### 维护性 +- **代码更简洁**:减少了不必要的菜单项代码 +- **功能更集中**:专注于核心功能 +- **更易维护**:减少了菜单相关的维护工作 + +## 功能保留说明 + +### 自动功能 +以下功能仍然自动运行,无需用户干预: +1. **日志优化**:自动减少日志打印频率 +2. **锁文件管理**:自动清理无效锁文件 +3. **缓存管理**:自动处理缓存冲突 +4. **错误处理**:自动处理各种异常情况 + +### 手动功能 +以下功能仍然可以通过托盘菜单使用: +1. **窗口控制**:显示/隐藏主窗口 +2. **配置管理**:配置客户端地址 +3. **悬浮窗口**:控制桌面悬浮窗口 +4. **系统操作**:更新、缓存清理、退出等 + +## 注意事项 + +### 1. 日志管理 +- 日志仍然正常记录和优化 +- 如果需要调整日志级别,可以通过代码修改 +- 日志文件仍然可以手动查看 + +### 2. 问题排查 +- 如果遇到启动问题,应用会自动处理 +- 如果遇到锁文件问题,应用会自动清理 +- 如果遇到缓存问题,可以使用"清除缓存"功能 + +### 3. 开发调试 +- 开发时可以通过代码设置日志级别 +- 调试时可以直接查看日志文件 +- 问题排查功能仍然完整 + +## 相关文件 +- `src/main/tray.js` - 托盘菜单(已简化) +- `src/main/utils/logger.js` - 日志系统(功能保留) +- `src/main/utils/singleInstance.js` - 单实例管理(功能保留) +- `src/main/utils/cacheUtils.js` - 缓存管理(功能保留) \ No newline at end of file diff --git a/src/main/index.js b/src/main/index.js index 66e98b2..be8ea68 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -3,28 +3,46 @@ import { electronApp, optimizer } from '@electron-toolkit/utils' import Store from 'electron-store' import { createWindow, createDrageWindow, unregisterAllShortcuts } from './window.js' import { setupIPC } from './ipc.js' -import { createTray, destroyTray } from './tray.js' -import { getStoreValue } from './store.js' +import { createTray, destroyTray,clearBrowserCache } from './tray.js' import XEUtils from 'xe-utils' - +import fs from 'fs' +import path from 'path' +import dayjs from 'dayjs' import logger from './utils/logger' - +import { clearAllSessionData } from './utils/cacheUtils.js' +import SingleInstanceManager from './utils/singleInstance.js' +import { setStoreValue, getStoreValue ,deleteStore} from './store.js' import AutoLaunch from 'auto-launch' import WebSocketClient from './utils/WebSocketClient'; let wsClient=null -wsClient=new WebSocketClient({ - autoReconnect: true, - autoReconnectAttempts: 9999, - autoReconnectInterval: 5000, - timeout: 30000, -}) +// 延迟创建WebSocket连接,等待应用完全启动后再连接 +function createWebSocketClient() { + try { + wsClient = new WebSocketClient({ + autoReconnect: true, + autoReconnectAttempts: 9999, + autoReconnectInterval: 5000, + timeout: 30000, + }); -wsClient.on('open', () => { - logger.info('WebSocket连接已打开') -}); + wsClient.on('open', () => { + logger.info('WebSocket连接已打开') + }); + + wsClient.on('error', (error) => { + logger.error('WebSocket连接错误:', error) + }); + + wsClient.on('close', (data) => { + logger.info('WebSocket连接已关闭:', data) + }); + } catch (error) { + logger.error('创建WebSocket客户端失败:', error) + } +} var minecraftAutoLauncher = new AutoLaunch({ @@ -53,40 +71,132 @@ if(!XEUtils.isEmpty(difySite)){ logger.info(`当前运行平台: ${process.platform}`) if (process.platform === 'win32') { logger.info('Windows 系统,设置控制台编码为 UTF-8') - require('child_process').execSync('chcp 65001', { stdio: 'ignore' }) + try { + require('child_process').execSync('chcp 65001', { stdio: 'ignore' }) + } catch (error) { + logger.error('设置控制台编码失败:', error) + } } else { logger.info('非 Windows 系统,使用默认编码') } logger.info('%cRed text. %cGreen text', 'color: red', 'color: green') -const store = new Store() +try { + const store = new Store() + logger.info('Store 初始化成功') +} catch (error) { + logger.error('Store 初始化失败:', error) +} app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors') app.commandLine.appendSwitch('ignore-certificate-errors') -app.disableHardwareAcceleration() +// 添加缓存相关配置,避免缓存错误 +app.commandLine.appendSwitch('disable-http-cache') +app.commandLine.appendSwitch('disable-background-timer-throttling') +app.commandLine.appendSwitch('disable-renderer-backgrounding') + +try { + app.disableHardwareAcceleration() + logger.info('硬件加速已禁用') +} catch (error) { + logger.error('禁用硬件加速失败:', error) +} logger.info(app.getPath('userData')) +logger.info('应用启动,日志文件路径:', logger.getLogPath()) + +// 添加更多启动信息 +logger.info('开始检查单实例锁...') + +// 创建单实例管理器 +const singleInstanceManager = new SingleInstanceManager() + +// 清理缓存目录 +function cleanupCacheDirectories() { + try { + const userDataPath = app.getPath('userData') + const cachePaths = [ + path.join(userDataPath, 'Cache'), + path.join(userDataPath, 'Code Cache'), + path.join(userDataPath, 'GPUCache'), + path.join(userDataPath, 'Service Worker'), + path.join(userDataPath, 'Session Storage') + ] + + for (const cachePath of cachePaths) { + if (fs.existsSync(cachePath)) { + logger.info(`清理缓存目录: ${cachePath}`) + try { + // 递归删除目录 + const deleteRecursive = (dirPath) => { + if (fs.existsSync(dirPath)) { + const files = fs.readdirSync(dirPath) + for (const file of files) { + const curPath = path.join(dirPath, file) + if (fs.lstatSync(curPath).isDirectory()) { + deleteRecursive(curPath) + } else { + try { + fs.unlinkSync(curPath) + } catch (error) { + logger.warn(`无法删除缓存文件: ${curPath}`, error.message) + } + } + } + try { + fs.rmdirSync(dirPath) + } catch (error) { + logger.warn(`无法删除缓存目录: ${dirPath}`, error.message) + } + } + } + + deleteRecursive(cachePath) + logger.info(`缓存目录清理完成: ${cachePath}`) + } catch (error) { + logger.error(`清理缓存目录失败: ${cachePath}`, error) + } + } + } + } catch (error) { + logger.error('清理缓存目录失败:', error) + } +} // 检查是否为第一个实例 -const gotTheLock = app.requestSingleInstanceLock() +const isFirstInstance = singleInstanceManager.checkSingleInstance() -if (!gotTheLock) { - // 如果不是第一个实例,尝试激活第一个实例的窗口 - const windows = BrowserWindow.getAllWindows() - if (windows.length > 0) { - const mainWindow = windows[0] - if (mainWindow.isMinimized()) { - mainWindow.restore() +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('尝试强制启动新实例...') + // 继续启动 } - mainWindow.show() - mainWindow.focus() + } catch (error) { + logger.error('激活现有实例失败:', error) + logger.info('尝试强制启动新实例...') + // 继续启动 } - app.quit() // 退出当前实例 } else { + logger.info('这是第一个实例,继续启动') // 这是第一个实例 // 监听第二个实例的启动 @@ -104,6 +214,10 @@ if (!gotTheLock) { }) app.whenReady().then(() => { + logger.info('应用已准备就绪,开始初始化...'); + + // 清理缓存目录 + cleanupCacheDirectories() // 获取默认会话并全局设置允许第三方 Cookie const defaultSession = session.defaultSession; @@ -134,6 +248,13 @@ if (!gotTheLock) { createTray() setupIPC() + // exe退出三秒之后即认为关闭过exe程序,退出登录 + checkIsKeepAlive() + + // 延迟创建WebSocket连接 + setTimeout(() => { + createWebSocketClient() + }, 2000); // 延迟2秒创建WebSocket连接 minecraftAutoLauncher.isEnabled() .then(function(isEnabled){ @@ -151,26 +272,134 @@ if (!gotTheLock) { app.on('activate', function () { if (BrowserWindow.getAllWindows().length === 0) createWindow() }) + + logger.info('应用初始化完成'); }) // 修改窗口关闭行为 - app.on('window-all-closed', () => { + app.on('window-all-closed', async (event) => { if (process.platform !== 'darwin') { + event.preventDefault(); if (!app.isQuiting) { + // 如果不是主动退出,则隐藏所有窗口 BrowserWindow.getAllWindows().forEach(window => { window.hide() }) + + // 清除所有会话数据 + try { + await clearAllSessionData('window-all-closed'); + } catch (error) { + logger.error('窗口关闭时清理缓存失败:', error); + } + + // 清理完成后退出应用 + app.quit(); + } else { // 如果是主动退出,则销毁托盘并退出应用 destroyTray() - app.quit() + } } }) + app.on('before-quit', async (event) => { + // 在应用程序即将退出时执行操作,例如保存数据 + event.preventDefault(); + + // 清除所有会话数据 + try { + await clearAllSessionData('before-quit'); + } catch (error) { + logger.error('应用退出时清理缓存失败:', error); + } + + // 清理完成后退出应用 + app.quit(); + }); + // 在应用退出时注销所有快捷键 - app.on('will-quit', () => { + app.on('will-quit', async (event) => { + + event.preventDefault(); + + // 清除所有会话数据 + try { + await clearAllSessionData('will-quit'); + } catch (error) { + logger.error('应用退出时清理缓存失败:', error); + } + unregisterAllShortcuts() + + // 清理完成后退出应用 + app.quit(); }) + + + + // 监听进程退出信号,确保在系统强制关闭时也能清理缓存 + process.on('SIGTERM', async () => { + logger.info('收到 SIGTERM 信号,开始清理缓存'); + try { + await clearAllSessionData('SIGTERM'); + } catch (error) { + logger.error('SIGTERM 时清理缓存失败:', error); + } + process.exit(0); + }); + + process.on('SIGINT', async () => { + logger.info('收到 SIGINT 信号,开始清理缓存'); + try { + await clearAllSessionData('SIGINT'); + } catch (error) { + logger.error('SIGINT 时清理缓存失败:', error); + } + process.exit(0); + }); + + // 监听未捕获的异常,确保应用崩溃时也能记录日志 + process.on('uncaughtException', (error) => { + logger.error('未捕获的异常:', error); + // 不要立即退出,给应用一个恢复的机会 + logger.error('应用遇到未捕获的异常,但将继续运行'); + }); + + process.on('unhandledRejection', (reason, promise) => { + logger.error('未处理的 Promise 拒绝:', reason); + // 不要立即退出,给应用一个恢复的机会 + logger.error('应用遇到未处理的 Promise 拒绝,但将继续运行'); + }); + } + + + +export function checkIsKeepAlive(){ + + const checkIsKeepAliveTimer=setInterval(async () => { + + const lastAliveTime = getStoreValue("lastAliveTime")||null + + if (lastAliveTime!=null){ + const nowTime=dayjs(); + const lastTime=dayjs(lastAliveTime); + const diff=nowTime.diff(lastTime, 'second') + + + // 上次在线事件在三秒之前,则认为关闭过exe程序 + if (diff>5){ + logger.info('上次在线事件在三秒之前,则认为关闭过exe程序') + await clearAllSessionData('超时5秒'); + deleteStore("lastAliveTime") + }else{ + setStoreValue("lastAliveTime",dayjs()) + } + } + + }, 1000 * 2) + +} \ No newline at end of file diff --git a/src/main/ipc.js b/src/main/ipc.js index ebe1e32..0e69d0f 100644 --- a/src/main/ipc.js +++ b/src/main/ipc.js @@ -10,7 +10,7 @@ import path from 'path'; log.initialize(); import {checkForUpdates} from "./utils/updateUtils" import logger from './utils/logger' - +import dayjs from 'dayjs' function isValidUrl(_url) { const containsApp = _url.includes('/app/'); @@ -34,9 +34,8 @@ export function setupIPC() { setStoreValue("difySite", data.difySite); - setStoreValue("test_12222222", "test_12222222"); - + setStoreValue("lastAliveTime",dayjs()) difyRetryRequestTimer() diff --git a/src/main/tray.js b/src/main/tray.js index eaf0a20..e6cdee1 100644 --- a/src/main/tray.js +++ b/src/main/tray.js @@ -1,38 +1,22 @@ -import { Tray, Menu, app, BrowserWindow } from 'electron' +import { Tray, Menu, app, BrowserWindow, shell } from 'electron' import { join } from 'path' import icon from '../../resources/icon.png?asset' import { getMainWindow, createWindow, createApiConfigWindow, createConfigWindow ,getDragWindow} from './window.js' import { getStoreValue, setStoreValue, clearStore } from './store.js' import { createDrageWindow, getDrageWindow } from './window.js' import logger from './utils/logger' +import { clearAllWindowsCache } from './utils/cacheUtils.js' import fs from 'fs' +import path from 'path' let tray = null import {checkForUpdates} from "./utils/updateUtils" -async function clearBrowserCache() { +export async function clearBrowserCache() { try { - // 清除所有窗口的浏览器缓存 - const windows = BrowserWindow.getAllWindows() - for (const window of windows) { - const session = window.webContents.session - await session.clearStorageData({ - storages: [ - 'appcache', - 'cookies', - 'filesystem', - 'indexdb', - 'localstorage', - 'shadercache', - 'websql', - 'serviceworkers', - 'cachestorage' - ] - }) - } - - logger.info('浏览器缓存清除成功') + // 使用统一的缓存清理函数 + await clearAllWindowsCache('tray-clear-cache'); } catch (error) { logger.error('清除浏览器缓存失败:', error) } @@ -108,6 +92,7 @@ export function createTray() { checkForUpdates(menuItem,true) } }, + { type: 'separator' }, { label: '清除缓存', diff --git a/src/main/utils/WebSocketClient.js b/src/main/utils/WebSocketClient.js index 557bd3b..2a92690 100644 --- a/src/main/utils/WebSocketClient.js +++ b/src/main/utils/WebSocketClient.js @@ -6,16 +6,19 @@ import { Notification } from "electron"; class WebSocketClient { constructor(options = {}) { - this.reconnectInterval = 5000; // 重连间隔时间 + this.reconnectInterval = options.reconnectInterval || 5000; // 重连间隔时间 this.lockReconnect = false; this.ws = null; this.pingTimeout = null; this.reconnectAttempts = 0; this.messageHandlers = new Map(); this.isConnected = false; + this.options = options; - // 初始化时立即创建连接 - this.createConnect(); + // 延迟创建连接,避免在应用启动时立即连接 + setTimeout(() => { + this.createConnect(); + }, 1000); } createConnect() { @@ -26,10 +29,10 @@ class WebSocketClient { // 检查必要的连接信息是否存在 if (!apiUrl || !userInfo || !token) { - logger.error("WebSocket连接信息不完整,等待重试"); - logger.error("apiUrl:", apiUrl); - logger.error("userInfo:", userInfo); - logger.error("token:", token); + logger.info("WebSocket连接信息不完整,等待重试"); + logger.info("apiUrl:", apiUrl); + logger.info("userInfo:", userInfo); + logger.info("token:", token); this.scheduleReconnect(); return; } @@ -92,10 +95,8 @@ class WebSocketClient { this.ws = new WebSocket(this.url, token, this.options); this.ws.on('open', () => { - logger.info('😀😀😀😀 WebSocket connect create ok 😀😀😀😀'); - logger.info('😀😀😀😀 WebSocket connect create ok 😀😀😀😀'); - logger.info('😀😀😀😀 WebSocket connect create ok 😀😀😀😀'); - logger.info('WebSocket protocol:', this.ws.protocol); // 添加协议日志 + logger.info('WebSocket 连接成功'); + logger.info('WebSocket protocol:', this.ws.protocol); this.isConnected = true; this.reconnectAttempts = 0; this.lockReconnect = false; @@ -105,8 +106,19 @@ class WebSocketClient { this.ws.on('message', (data) => { try { - logger.info('收到消息:', data.toString()); - const message = JSON.parse(data); + const messageStr = data.toString(); + const message = JSON.parse(messageStr); + + // 减少心跳消息的日志打印 + if (message.cmd === 'heartcheck') { + // 心跳消息不打印日志,或者只在调试模式下打印 + if (process.env.NODE_ENV === 'development') { + logger.debug('收到心跳消息'); + } + } else { + logger.info('收到消息:', messageStr); + } + this.handleMessage(message); } catch (error) { logger.error('消息解析错误:', error); diff --git a/src/main/utils/cacheUtils.js b/src/main/utils/cacheUtils.js new file mode 100644 index 0000000..3fde853 --- /dev/null +++ b/src/main/utils/cacheUtils.js @@ -0,0 +1,93 @@ +import { session } from 'electron' +import logger from './logger.js' + +/** + * 清除所有会话数据的统一函数 + * @param {string} context - 清理上下文,用于日志记录 + * @returns {Promise} + */ +export async function clearAllSessionData(context = 'unknown') { + logger.info(`[${context}] 开始清除所有会话数据`); + + try { + await new Promise((resolve, reject) => { + session.defaultSession.clearStorageData({ + storages: [ + 'appcache', + 'cookies', + 'filesystem', + 'indexdb', + 'localstorage', + 'shadercache', + 'websql', + 'serviceworkers', + 'cachestorage' + ] + }, (error) => { + if (error) { + logger.error(`[${context}] 清除会话数据失败:`, error); + reject(error); + } else { + logger.info(`[${context}] 会话数据清除成功`); + resolve(); + } + }); + }); + + // 强制同步日志 + logger.info(`[${context}] 缓存清理完成`); + + // 等待一小段时间确保日志写入 + await new Promise(resolve => setTimeout(resolve, 100)); + + } catch (error) { + logger.error(`[${context}] 清理缓存失败:`, error); + throw error; + } +} + +/** + * 清除所有窗口的浏览器缓存 + * @param {string} context - 清理上下文,用于日志记录 + * @returns {Promise} + */ +export async function clearAllWindowsCache(context = 'unknown') { + logger.info(`[${context}] 开始清除所有窗口的浏览器缓存`); + + try { + const { BrowserWindow } = require('electron'); + const windows = BrowserWindow.getAllWindows(); + + for (const window of windows) { + const session = window.webContents.session; + await new Promise((resolve, reject) => { + session.clearStorageData({ + storages: [ + 'appcache', + 'cookies', + 'filesystem', + 'indexdb', + 'localstorage', + 'shadercache', + 'websql', + 'serviceworkers', + 'cachestorage' + ] + }, (error) => { + if (error) { + logger.error(`[${context}] 清除窗口缓存失败:`, error); + reject(error); + } else { + resolve(); + } + }); + }); + } + + logger.info(`[${context}] 所有窗口缓存清除成功`); + + } catch (error) { + logger.error(`[${context}] 清除窗口缓存失败:`, error); + throw error; + } +} \ No newline at end of file diff --git a/src/main/utils/logger.js b/src/main/utils/logger.js index a745033..bd47905 100644 --- a/src/main/utils/logger.js +++ b/src/main/utils/logger.js @@ -1,14 +1,72 @@ import log from 'electron-log/main'; +import { app } from 'electron'; +import path from 'path'; log.initialize(); +// 配置日志立即写入 +log.transports.file.level = 'info'; +log.transports.console.level = 'info'; + +// 确保日志立即写入文件 +log.transports.file.sync = true; + +// 获取日志文件路径 +const logFilePath = log.transports.file.getFile().path; +log.info(`日志文件路径: ${logFilePath}`); + +// 日志级别配置 +const LOG_LEVELS = { + ERROR: 0, + WARN: 1, + INFO: 2, + DEBUG: 3 +}; + +let currentLogLevel = LOG_LEVELS.INFO; // 默认日志级别 + +// 设置日志级别 +export function setLogLevel(level) { + if (typeof level === 'string') { + currentLogLevel = LOG_LEVELS[level.toUpperCase()] || LOG_LEVELS.INFO; + } else { + currentLogLevel = level; + } +} + +// 检查是否应该打印日志 +function shouldLog(level) { + return level <= currentLogLevel; +} export const logger = { - log: log.info, - info: log.info, - error: log.error, - warn: log.warn, - debug: log.debug + log: (message, ...args) => { + if (shouldLog(LOG_LEVELS.INFO)) { + log.info(message, ...args); + } + }, + info: (message, ...args) => { + if (shouldLog(LOG_LEVELS.INFO)) { + log.info(message, ...args); + } + }, + error: (message, ...args) => { + if (shouldLog(LOG_LEVELS.ERROR)) { + log.error(message, ...args); + } + }, + warn: (message, ...args) => { + if (shouldLog(LOG_LEVELS.WARN)) { + log.warn(message, ...args); + } + }, + debug: (message, ...args) => { + if (shouldLog(LOG_LEVELS.DEBUG)) { + log.debug(message, ...args); + } + }, + getLogPath: () => logFilePath, + setLogLevel: setLogLevel } export default logger diff --git a/src/main/utils/singleInstance.js b/src/main/utils/singleInstance.js new file mode 100644 index 0000000..0aa5f26 --- /dev/null +++ b/src/main/utils/singleInstance.js @@ -0,0 +1,205 @@ +import { app } from 'electron' +import fs from 'fs' +import path from 'path' +import logger from './logger.js' + +/** + * 单实例锁管理工具 + */ +export class SingleInstanceManager { + constructor() { + this.lockFile = null + this.isLocked = false + } + + /** + * 清理可能存在的无效单实例锁 + */ + 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) { + return false + } + } + + /** + * 创建锁文件 + */ + createLock() { + try { + const userDataPath = app.getPath('userData') + this.lockFile = path.join(userDataPath, 'single-instance-lock') + + // 写入当前进程的 PID + fs.writeFileSync(this.lockFile, process.pid.toString()) + this.isLocked = true + + logger.info(`创建单实例锁文件: ${this.lockFile}, PID: ${process.pid}`) + + // 监听应用退出事件,清理锁文件 + app.on('before-quit', () => { + this.removeLock() + }) + + app.on('will-quit', () => { + this.removeLock() + }) + + // 监听进程退出信号 + process.on('exit', () => { + this.removeLock() + }) + + process.on('SIGINT', () => { + this.removeLock() + process.exit(0) + }) + + process.on('SIGTERM', () => { + this.removeLock() + process.exit(0) + }) + + } catch (error) { + logger.error('创建锁文件失败:', error) + } + } + + /** + * 移除锁文件 + */ + removeLock() { + if (this.lockFile && this.isLocked) { + try { + if (fs.existsSync(this.lockFile)) { + fs.unlinkSync(this.lockFile) + logger.info(`移除锁文件: ${this.lockFile}`) + } + this.isLocked = false + } catch (error) { + logger.error('移除锁文件失败:', error) + } + } + } + + /** + * 检查是否为第一个实例 + */ + 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/src/main/window.js b/src/main/window.js index b627ac6..a54f0d3 100644 --- a/src/main/window.js +++ b/src/main/window.js @@ -11,13 +11,12 @@ import { setStoreValue, getStoreValue,deleteStore } from './store.js' import {checkForUpdates} from "./utils/updateUtils" import dayjs from 'dayjs' - let mainWindow = null let difyfullScreenWindow = null let drageWindow = null let apiConfigWindow = null let configWindow = null - +import { clearAllSessionData } from './utils/cacheUtils.js' // 权限授权 async function checkMediaAccess(mediaType){ const result = systemPreferences.getMediaAccessStatus(mediaType) @@ -109,6 +108,9 @@ export async function createWindow() { mainWindow.webContents.on('before-input-event', (event, input) => { if (input.key === 'F12') { mainWindow.webContents.toggleDevTools() + } else if (input.key === 'F5') { + logger.info('主窗口 F5 快捷键触发') + mainWindow.reload() } }) @@ -164,9 +166,11 @@ export async function createWindow() { // 加载存储的 URL mainWindow.loadURL(h5_client_url) - // 超过30分钟不活动,则退出登录 + // 接口超过30分钟不活动,则退出登录 await tokenExpireTimer() + + setTimeout(()=>{ try { // 注册全局快捷键 @@ -189,21 +193,44 @@ export async function createWindow() { }); + } // 添加一个专门的快捷键注册函数 -function registerShortcuts(window) { +function registerShortcuts(window=null) { + // 先注销可能存在的F5快捷键 + globalShortcut.unregister('F5') + // 注册 F5 刷新快捷键 - globalShortcut.register('F5', () => { + const success = globalShortcut.register('F5', () => { logger.info('F5 快捷键触发') - if (window && !window.isDestroyed()) { - window.reload() + + try { + // 获取当前焦点窗口 + const focusedWindow = BrowserWindow.getFocusedWindow() + logger.info('当前焦点窗口:', focusedWindow ? '存在' : '不存在') + + if (focusedWindow && !focusedWindow.isDestroyed()) { + logger.info('刷新当前焦点窗口') + focusedWindow.reload() + } else if (mainWindow && !mainWindow.isDestroyed()) { + logger.info('没有焦点窗口,刷新主窗口') + mainWindow.reload() + } else if (difyfullScreenWindow && !difyfullScreenWindow.isDestroyed()) { + logger.info('主窗口不可用,刷新全屏窗口') + difyfullScreenWindow.reload() + } else { + logger.warn('没有可用的窗口进行刷新') + } + } catch (error) { + logger.error('F5快捷键执行出错:', error) } }) - + + logger.info(`F5快捷键注册${success ? '成功' : '失败'}`) const isRegistered_F12 = globalShortcut.isRegistered('F12'); - logger.info(`Is CommandOrControl+X registered: ${isRegistered_F12}`); + logger.info(`Is F12 registered: ${isRegistered_F12}`); // 桌面端快要退出的时候,注销快捷键 app.on('will-quit', () => { @@ -236,6 +263,8 @@ export async function createNewWindow(url, access_token, refresh_token,sandbox=f difyfullScreenWindow.on('ready-to-show', () => { difyfullScreenWindow.show() + // 确保快捷键在新窗口显示后也能工作 + registerShortcuts(difyfullScreenWindow) }) let code = `localStorage.setItem("IsHsAiApp","IsHsAiApp");localStorage.setItem("console_token","${access_token}");localStorage.setItem("refresh_token","12");` @@ -298,6 +327,9 @@ export async function createNewWindow(url, access_token, refresh_token,sandbox=f difyfullScreenWindow.webContents.on('before-input-event', (event, input) => { if (input.key === 'F12') { difyfullScreenWindow.webContents.toggleDevTools() + } else if (input.key === 'F5') { + logger.info('全屏窗口 F5 快捷键触发') + difyfullScreenWindow.reload() } }) @@ -467,18 +499,29 @@ export function createDrageWindow() { drageWindow.setPosition(screenWidth - drageWindow.getSize()[0] -100, screenHeight - drageWindow.getSize()[1] - 100) } + export async function tokenExpireTimer(){ + let lastLogTime = 0; // 记录上次打印日志的时间 + const LOG_INTERVAL = 60000; // 日志打印间隔,60秒打印一次 + const tokenExpireTimer = setInterval(async () => { - logger.info("tokenExpireTimer 触发") + const currentTime = Date.now(); const lastActiveTime = getStoreValue("lastActiveTime")||null + if (lastActiveTime!=null){ - logger.info("tokenExpireTimer 触发 对比时间戳") try { const nowTime=dayjs(); const lastTime=dayjs(lastActiveTime); const diff=nowTime.diff(lastTime, 'minute') - logger.info("tokenExpireTimer 触发 对比时间戳差距为:"+diff) + + // 只在特定条件下打印日志,减少日志频率 + if (currentTime - lastLogTime > LOG_INTERVAL) { + logger.info(`tokenExpireTimer 检查 - 时间差距: ${diff}分钟`) + lastLogTime = currentTime; + } + if ( diff> 30) { + logger.info(`用户超时退出登录 - 时间差距: ${diff}分钟`) deleteStore("lastActiveTime") try { // 清除所有窗口的浏览器缓存 @@ -512,7 +555,7 @@ export async function tokenExpireTimer(){ } } catch (e) { - + logger.error('tokenExpireTimer 执行错误:', e) } } }, 1000 * 10) @@ -610,3 +653,32 @@ export function closeConfigWindow() { configWindow = null } } + +// 测试快捷键是否正常工作 +export function testShortcuts() { + logger.info('测试快捷键功能...') + + // 检查F5快捷键是否已注册 + const isF5Registered = globalShortcut.isRegistered('F5') + logger.info(`F5快捷键是否已注册: ${isF5Registered}`) + + // 获取所有已注册的快捷键 + const allShortcuts = globalShortcut.isRegistered('F5') ? ['F5'] : [] + logger.info(`已注册的快捷键: ${allShortcuts.join(', ')}`) + + // 获取当前焦点窗口 + const focusedWindow = BrowserWindow.getFocusedWindow() + logger.info(`当前焦点窗口: ${focusedWindow ? '存在' : '不存在'}`) + + if (focusedWindow) { + logger.info(`焦点窗口标题: ${focusedWindow.getTitle()}`) + logger.info(`焦点窗口是否销毁: ${focusedWindow.isDestroyed()}`) + } + + return { + f5Registered: isF5Registered, + focusedWindow: focusedWindow ? 'exists' : 'none', + mainWindow: mainWindow && !mainWindow.isDestroyed() ? 'exists' : 'none', + difyWindow: difyfullScreenWindow && !difyfullScreenWindow.isDestroyed() ? 'exists' : 'none' + } +} diff --git a/test-cache-clear.js b/test-cache-clear.js new file mode 100644 index 0000000..e24f880 --- /dev/null +++ b/test-cache-clear.js @@ -0,0 +1,44 @@ +const { app, session } = require('electron'); + +// 模拟缓存清理测试 +async function testCacheClear() { + console.log('开始测试缓存清理功能...'); + + try { + // 测试清除所有会话数据 + await new Promise((resolve, reject) => { + session.defaultSession.clearStorageData({ + storages: [ + 'appcache', + 'cookies', + 'filesystem', + 'indexdb', + 'localstorage', + 'shadercache', + 'websql', + 'serviceworkers', + 'cachestorage' + ] + }, (error) => { + if (error) { + console.error('清除会话数据失败:', error); + reject(error); + } else { + console.log('会话数据清除成功'); + resolve(); + } + }); + }); + + console.log('缓存清理测试完成'); + } catch (error) { + console.error('缓存清理测试失败:', error); + } +} + +// 如果直接运行此脚本 +if (require.main === module) { + testCacheClear(); +} + +module.exports = { testCacheClear }; \ No newline at end of file diff --git a/test-lock-cleanup.js b/test-lock-cleanup.js new file mode 100644 index 0000000..a6a081a --- /dev/null +++ b/test-lock-cleanup.js @@ -0,0 +1,138 @@ +const { app } = require('electron'); +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +console.log('开始测试锁文件清理功能...'); + +// 测试安全删除文件功能 +function testSafeDeleteFile(filePath) { + console.log(`测试删除文件: ${filePath}`); + + try { + // 首先尝试直接删除 + fs.unlinkSync(filePath); + console.log(`成功删除文件: ${filePath}`); + } catch (error) { + if (error.code === 'EBUSY' || error.code === 'EACCES') { + console.log(`文件被占用,无法删除: ${filePath}`); + console.log(`错误信息: ${error.message}`); + + // 在 Windows 上尝试使用 del 命令 + if (process.platform === 'win32') { + try { + execSync(`del /F /Q "${filePath}"`, { stdio: 'ignore' }); + console.log(`使用 del 命令删除文件: ${filePath}`); + } catch (delError) { + console.error(`del 命令删除失败: ${filePath}`, delError); + } + } + } else { + console.error(`删除文件失败: ${filePath}`, error); + } + } +} + +// 测试创建和删除锁文件 +function testLockFileOperations() { + const userDataPath = app.getPath('userData'); + const testLockFile = path.join(userDataPath, 'test-lockfile'); + + console.log('用户数据路径:', userDataPath); + console.log('测试锁文件路径:', testLockFile); + + try { + // 创建测试锁文件 + fs.writeFileSync(testLockFile, process.pid.toString()); + console.log(`创建测试锁文件: ${testLockFile}, PID: ${process.pid}`); + + // 等待一段时间 + setTimeout(() => { + console.log('开始测试删除锁文件...'); + testSafeDeleteFile(testLockFile); + + // 检查文件是否还存在 + if (fs.existsSync(testLockFile)) { + console.log('文件仍然存在,尝试强制删除...'); + if (process.platform === 'win32') { + try { + execSync(`del /F /Q "${testLockFile}"`, { stdio: 'ignore' }); + console.log('强制删除成功'); + } catch (error) { + console.error('强制删除失败:', error); + } + } + } else { + console.log('文件已成功删除'); + } + + process.exit(0); + }, 2000); + + } catch (error) { + console.error('创建测试锁文件失败:', error); + process.exit(1); + } +} + +// 测试检查现有锁文件 +function testExistingLockFiles() { + const userDataPath = app.getPath('userData'); + const possibleLockFiles = [ + path.join(userDataPath, 'single-instance-lock'), + path.join(userDataPath, 'lockfile'), + path.join(userDataPath, '.lock') + ]; + + console.log('检查现有锁文件...'); + + for (const lockFile of possibleLockFiles) { + if (fs.existsSync(lockFile)) { + console.log(`发现锁文件: ${lockFile}`); + try { + const content = fs.readFileSync(lockFile, 'utf8'); + console.log(`锁文件内容: ${content}`); + + const pid = parseInt(content.trim()); + if (pid) { + console.log(`锁文件中的 PID: ${pid}`); + + // 检查进程是否还在运行 + if (process.platform === 'win32') { + try { + const result = execSync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`, { encoding: 'utf8' }); + if (result.includes(pid.toString())) { + console.log(`进程 ${pid} 仍在运行`); + } else { + console.log(`进程 ${pid} 不存在,可以删除锁文件`); + testSafeDeleteFile(lockFile); + } + } catch (error) { + console.log(`进程 ${pid} 不存在,删除锁文件`); + testSafeDeleteFile(lockFile); + } + } + } + } catch (error) { + console.error(`读取锁文件失败: ${lockFile}`, error); + testSafeDeleteFile(lockFile); + } + } else { + console.log(`没有找到锁文件: ${lockFile}`); + } + } +} + +// 运行测试 +console.log('当前进程 PID:', process.pid); + +// 检查现有锁文件 +testExistingLockFiles(); + +// 如果直接运行此脚本 +if (require.main === module) { + // 测试锁文件操作 + testLockFileOperations(); +} + +module.exports = { testSafeDeleteFile, testExistingLockFiles, testLockFileOperations }; \ No newline at end of file diff --git a/test-single-instance.js b/test-single-instance.js new file mode 100644 index 0000000..0e4f1f4 --- /dev/null +++ b/test-single-instance.js @@ -0,0 +1,87 @@ +const { app } = require('electron'); +const fs = require('fs'); +const path = require('path'); + +console.log('开始测试单实例锁功能...'); + +// 模拟单实例锁检查 +function testSingleInstanceLock() { + const userDataPath = app.getPath('userData'); + const lockFile = path.join(userDataPath, 'single-instance-lock'); + + console.log('用户数据路径:', userDataPath); + console.log('锁文件路径:', lockFile); + + // 检查是否存在锁文件 + if (fs.existsSync(lockFile)) { + console.log('发现锁文件,尝试读取内容...'); + try { + const content = fs.readFileSync(lockFile, 'utf8'); + console.log('锁文件内容:', content); + + // 检查进程是否还在运行 + const pid = parseInt(content.trim()); + console.log('锁文件中的 PID:', pid); + + // 在 Windows 上检查进程 + if (process.platform === 'win32') { + const { execSync } = require('child_process'); + try { + const result = execSync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`, { encoding: 'utf8' }); + if (result.includes(pid.toString())) { + console.log(`进程 ${pid} 仍在运行`); + } else { + console.log(`进程 ${pid} 不存在,可以清理锁文件`); + fs.unlinkSync(lockFile); + console.log('锁文件已清理'); + } + } catch (error) { + console.log(`进程 ${pid} 不存在,清理锁文件`); + fs.unlinkSync(lockFile); + console.log('锁文件已清理'); + } + } + } catch (error) { + console.error('读取锁文件失败:', error); + } + } else { + console.log('没有找到锁文件'); + } +} + +// 测试创建锁文件 +function testCreateLock() { + const userDataPath = app.getPath('userData'); + const lockFile = path.join(userDataPath, 'single-instance-lock'); + + try { + fs.writeFileSync(lockFile, process.pid.toString()); + console.log(`创建锁文件: ${lockFile}, PID: ${process.pid}`); + } catch (error) { + console.error('创建锁文件失败:', error); + } +} + +// 运行测试 +console.log('当前进程 PID:', process.pid); +testSingleInstanceLock(); + +// 如果直接运行此脚本 +if (require.main === module) { + // 创建测试锁文件 + testCreateLock(); + + // 等待一段时间后清理 + setTimeout(() => { + console.log('清理测试锁文件...'); + const userDataPath = app.getPath('userData'); + const lockFile = path.join(userDataPath, 'single-instance-lock'); + if (fs.existsSync(lockFile)) { + fs.unlinkSync(lockFile); + console.log('测试锁文件已清理'); + } + process.exit(0); + }, 5000); +} + +module.exports = { testSingleInstanceLock, testCreateLock }; \ No newline at end of file diff --git a/test-startup.js b/test-startup.js new file mode 100644 index 0000000..4263d9b --- /dev/null +++ b/test-startup.js @@ -0,0 +1,42 @@ +const { app } = require('electron'); +const path = require('path'); + +console.log('开始测试应用启动...'); + +// 模拟应用启动过程 +app.on('ready', () => { + console.log('应用已准备就绪'); +}); + +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('应用将要退出'); +}); + +// 检查单实例锁 +const gotTheLock = app.requestSingleInstanceLock(); +console.log('单实例锁检查结果:', gotTheLock); + +if (!gotTheLock) { + console.log('检测到已有实例运行,退出当前实例'); + app.quit(); +} else { + console.log('这是第一个实例,继续启动'); + + // 模拟异步操作 + setTimeout(() => { + console.log('异步操作完成'); + }, 1000); +} + +console.log('启动脚本执行完成'); \ No newline at end of file