优化
This commit is contained in:
@@ -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
|
VITE_HsAppCode=1
|
||||||
|
|||||||
265
EXIT_FUNCTION_FIX.md
Normal file
265
EXIT_FUNCTION_FIX.md
Normal file
@@ -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()`、添加窗口销毁延迟、提供多层退出保护机制,成功解决了应用退出时进程残留的问题。现在点击托盘"退出应用"后,应用将完全退出,无进程残留,可以正常重新启动。
|
||||||
152
SINGLE_INSTANCE_FINAL_FIX.md
Normal file
152
SINGLE_INSTANCE_FINAL_FIX.md
Normal file
@@ -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. 生产环境
|
||||||
|
- 确保应用正常退出时能清理资源
|
||||||
|
- 监控应用的单实例行为
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
通过统一应用标识符、简化单实例实现逻辑、清理残留进程,成功解决了编译后可以打开多个实例的问题。现在应用严格按照单实例模式运行,符合桌面应用的标准行为。
|
||||||
94
cleanup-lock-files.js
Normal file
94
cleanup-lock-files.js
Normal file
@@ -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清理完成!现在可以重新启动应用了。')
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
appId: com.huashiai.app
|
appId: com.huashiai.dify-market-manager-gui
|
||||||
compression: maximum
|
compression: maximum
|
||||||
productName: 福田区百千万AI智能体共创平台
|
productName: 福田区百千万AI智能体共创平台
|
||||||
directories:
|
directories:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
appId: com.huashiai.app
|
appId: com.huashiai.dify-market-manager-gui
|
||||||
compression: maximum
|
compression: maximum
|
||||||
productName: 百千万AI智能体共创平台
|
productName: 百千万AI智能体共创平台
|
||||||
directories:
|
directories:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
appId: com.huashiai.app
|
appId: com.huashiai.dify-market-manager-gui
|
||||||
compression: maximum
|
compression: maximum
|
||||||
productName: 苏小胭
|
productName: 苏小胭
|
||||||
directories:
|
directories:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
appId: com.huashiai.app
|
appId: com.huashiai.dify-market-manager-gui
|
||||||
compression: maximum
|
compression: maximum
|
||||||
productName: 龙岗区百千万AI智能体共创平台
|
productName: 龙岗区百千万AI智能体共创平台
|
||||||
directories:
|
directories:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
appId: com.huashiai.app
|
appId: com.huashiai.dify-market-manager-gui
|
||||||
compression: maximum
|
compression: maximum
|
||||||
productName: 龙岗区百千万AI智能体共创平台
|
productName: 龙岗区百千万AI智能体共创平台
|
||||||
directories:
|
directories:
|
||||||
|
|||||||
73
force-kill-processes.js
Normal file
73
force-kill-processes.js
Normal file
@@ -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清理完成!现在可以重新启动应用了。')
|
||||||
38
kill-processes.js
Normal file
38
kill-processes.js
Normal file
@@ -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)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "market-manager-gui",
|
"name": "dify-market-manager-gui",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "百千万AI智能体共创平台",
|
"description": "百千万AI智能体共创平台",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
|
|||||||
@@ -109,10 +109,7 @@ logger.info(app.getPath('userData'))
|
|||||||
logger.info('应用启动,日志文件路径:', logger.getLogPath())
|
logger.info('应用启动,日志文件路径:', logger.getLogPath())
|
||||||
|
|
||||||
// 添加更多启动信息
|
// 添加更多启动信息
|
||||||
logger.info('开始检查单实例锁...')
|
logger.info('开始初始化应用...')
|
||||||
|
|
||||||
// 创建单实例管理器
|
|
||||||
const singleInstanceManager = new SingleInstanceManager()
|
|
||||||
|
|
||||||
// 清理缓存目录
|
// 清理缓存目录
|
||||||
function cleanupCacheDirectories() {
|
function cleanupCacheDirectories() {
|
||||||
@@ -166,43 +163,25 @@ function cleanupCacheDirectories() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单应用启动实现
|
||||||
|
* 请求单一实例锁,
|
||||||
|
* 如果该方法返回`false`,
|
||||||
|
* 则表示已经有一个实例在运行,
|
||||||
|
* 可以通过`app.quit()`方法退出当前实例。
|
||||||
|
*/
|
||||||
|
const gotTheLock = app.requestSingleInstanceLock()
|
||||||
|
|
||||||
|
if (!gotTheLock) {
|
||||||
// 检查是否为第一个实例
|
logger.info('检测到已有实例运行,退出当前实例')
|
||||||
const isFirstInstance = singleInstanceManager.checkSingleInstance()
|
app.quit()
|
||||||
|
|
||||||
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() // 强制退出,避免创建重复托盘
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
logger.info('这是第一个实例,继续启动')
|
logger.info('这是第一个实例,继续启动')
|
||||||
// 这是第一个实例
|
|
||||||
|
|
||||||
// 监听第二个实例的启动
|
// 监听第二个实例被运行时
|
||||||
app.on('second-instance', (event, commandLine, workingDirectory) => {
|
app.on('second-instance', (event, commandLine, workingDirectory) => {
|
||||||
// 当运行第二个实例时,将显示第一个实例的窗口
|
logger.info('检测到第二个实例启动,激活现有实例')
|
||||||
|
// 当有第二个实例被运行时,激活之前的实例并将焦点置于其窗口
|
||||||
const windows = BrowserWindow.getAllWindows()
|
const windows = BrowserWindow.getAllWindows()
|
||||||
if (windows.length > 0) {
|
if (windows.length > 0) {
|
||||||
const mainWindow = windows[0]
|
const mainWindow = windows[0]
|
||||||
@@ -228,7 +207,7 @@ if (!isFirstInstance) {
|
|||||||
callback(true);
|
callback(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
electronApp.setAppUserModelId('com.electron')
|
electronApp.setAppUserModelId('com.huashiai.dify-market-manager-gui')
|
||||||
|
|
||||||
app.on('browser-window-created', (_, window) => {
|
app.on('browser-window-created', (_, window) => {
|
||||||
optimizer.watchWindowShortcuts(window)
|
optimizer.watchWindowShortcuts(window)
|
||||||
@@ -281,7 +260,7 @@ if (!isFirstInstance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!app.isQuiting) {
|
if (!app.isQuiting) {
|
||||||
|
logger.info('用户关闭窗口,隐藏应用')
|
||||||
// 如果不是主动退出,则隐藏所有窗口
|
// 如果不是主动退出,则隐藏所有窗口
|
||||||
BrowserWindow.getAllWindows().forEach(window => {
|
BrowserWindow.getAllWindows().forEach(window => {
|
||||||
window.hide()
|
window.hide()
|
||||||
@@ -299,9 +278,11 @@ if (!isFirstInstance) {
|
|||||||
app.quit();
|
app.quit();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
logger.info('主动退出,完全关闭应用')
|
||||||
// 如果是主动退出,则销毁托盘并退出应用
|
// 如果是主动退出,则销毁托盘并退出应用
|
||||||
destroyTray()
|
destroyTray()
|
||||||
app.quit()
|
// 使用 exit 确保完全退出
|
||||||
|
app.exit(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -313,6 +294,7 @@ if (!isFirstInstance) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info('应用即将退出,开始清理资源')
|
||||||
// 在应用程序即将退出时执行操作,例如保存数据
|
// 在应用程序即将退出时执行操作,例如保存数据
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@@ -325,7 +307,8 @@ if (!isFirstInstance) {
|
|||||||
|
|
||||||
// 销毁托盘并退出应用
|
// 销毁托盘并退出应用
|
||||||
destroyTray()
|
destroyTray()
|
||||||
app.quit();
|
// 使用 exit 确保完全退出
|
||||||
|
app.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 在应用退出时注销所有快捷键
|
// 在应用退出时注销所有快捷键
|
||||||
@@ -336,6 +319,7 @@ if (!isFirstInstance) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info('应用即将退出,注销快捷键')
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
// 清除所有会话数据
|
// 清除所有会话数据
|
||||||
@@ -348,12 +332,10 @@ if (!isFirstInstance) {
|
|||||||
unregisterAllShortcuts()
|
unregisterAllShortcuts()
|
||||||
destroyTray()
|
destroyTray()
|
||||||
|
|
||||||
// 清理完成后退出应用
|
// 清理完成后退出应用,使用 exit 确保完全退出
|
||||||
app.quit();
|
app.exit(0);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 监听进程退出信号,确保在系统强制关闭时也能清理缓存
|
// 监听进程退出信号,确保在系统强制关闭时也能清理缓存
|
||||||
process.on('SIGTERM', async () => {
|
process.on('SIGTERM', async () => {
|
||||||
logger.info('收到 SIGTERM 信号,开始清理缓存');
|
logger.info('收到 SIGTERM 信号,开始清理缓存');
|
||||||
@@ -389,11 +371,8 @@ if (!isFirstInstance) {
|
|||||||
// 不要立即退出,给应用一个恢复的机会
|
// 不要立即退出,给应用一个恢复的机会
|
||||||
logger.error('应用遇到未处理的 Promise 拒绝,但将继续运行');
|
logger.error('应用遇到未处理的 Promise 拒绝,但将继续运行');
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function checkIsKeepAlive(){
|
export function checkIsKeepAlive(){
|
||||||
|
|
||||||
const checkIsKeepAliveTimer=setInterval(async () => {
|
const checkIsKeepAliveTimer=setInterval(async () => {
|
||||||
|
|||||||
@@ -174,12 +174,41 @@ export function createTray() {
|
|||||||
{
|
{
|
||||||
label: '退出应用',
|
label: '退出应用',
|
||||||
click: () => {
|
click: () => {
|
||||||
|
logger.info('用户点击退出应用')
|
||||||
app.isQuiting = true
|
app.isQuiting = true
|
||||||
|
|
||||||
// 确保所有窗口都被关闭
|
// 确保所有窗口都被关闭
|
||||||
BrowserWindow.getAllWindows().forEach(window => {
|
const windows = BrowserWindow.getAllWindows()
|
||||||
window.destroy()
|
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确保窗口销毁完成
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import path from 'path'
|
|||||||
import logger from './logger.js'
|
import logger from './logger.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 单实例锁管理工具
|
* 简化的单实例管理器
|
||||||
|
* 使用 Electron 内置的 requestSingleInstanceLock 方法
|
||||||
*/
|
*/
|
||||||
export class SingleInstanceManager {
|
export class SingleInstanceManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -13,98 +14,27 @@ export class SingleInstanceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清理可能存在的无效单实例锁
|
* 检查是否为第一个实例
|
||||||
|
* 使用 Electron 内置的单实例锁机制
|
||||||
*/
|
*/
|
||||||
cleanupInvalidLock() {
|
checkSingleInstance() {
|
||||||
try {
|
// 直接使用 Electron 的单实例锁
|
||||||
const userDataPath = app.getPath('userData')
|
const gotTheLock = app.requestSingleInstanceLock()
|
||||||
const possibleLockFiles = [
|
logger.info(`单实例锁检查结果: ${gotTheLock}`)
|
||||||
path.join(userDataPath, 'single-instance-lock'),
|
|
||||||
path.join(userDataPath, 'lockfile'),
|
if (gotTheLock) {
|
||||||
path.join(userDataPath, '.lock'),
|
// 成功获取锁,创建锁文件作为备份
|
||||||
path.join(process.cwd(), 'single-instance-lock'),
|
this.createLock()
|
||||||
path.join(process.cwd(), 'lockfile'),
|
return true
|
||||||
path.join(process.cwd(), '.lock')
|
} else {
|
||||||
]
|
// 无法获取锁,说明已有实例运行
|
||||||
|
logger.info('检测到已有实例运行,阻止新实例启动')
|
||||||
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
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建锁文件
|
* 创建锁文件作为备份
|
||||||
*/
|
*/
|
||||||
createLock() {
|
createLock() {
|
||||||
try {
|
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
|
export default SingleInstanceManager
|
||||||
154
test-exit-function.js
Normal file
154
test-exit-function.js
Normal file
@@ -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,<h1>退出功能测试</h1><p>PID: ' + process.pid + '</p><p>时间: ' + new Date().toLocaleString() + '</p><p>点击托盘图标右键菜单中的"退出应用"来测试退出功能</p>')
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
66
test-single-instance-simple.js
Normal file
66
test-single-instance-simple.js
Normal file
@@ -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,<h1>单实例测试</h1><p>PID: ' + process.pid + '</p><p>时间: ' + new Date().toLocaleString() + '</p>')
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user