This commit is contained in:
2025-07-22 17:53:26 +08:00
parent f667c26650
commit e43f850840
17 changed files with 924 additions and 182 deletions

View File

@@ -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

265
EXIT_FUNCTION_FIX.md Normal file
View 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()`、添加窗口销毁延迟、提供多层退出保护机制,成功解决了应用退出时进程残留的问题。现在点击托盘"退出应用"后,应用将完全退出,无进程残留,可以正常重新启动。

View 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
View 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清理完成现在可以重新启动应用了。')

View File

@@ -1,4 +1,4 @@
appId: com.huashiai.app
appId: com.huashiai.dify-market-manager-gui
compression: maximum
productName: 福田区百千万AI智能体共创平台
directories:

View File

@@ -1,4 +1,4 @@
appId: com.huashiai.app
appId: com.huashiai.dify-market-manager-gui
compression: maximum
productName: 百千万AI智能体共创平台
directories:

View File

@@ -1,4 +1,4 @@
appId: com.huashiai.app
appId: com.huashiai.dify-market-manager-gui
compression: maximum
productName: 苏小胭
directories:

View File

@@ -1,4 +1,4 @@
appId: com.huashiai.app
appId: com.huashiai.dify-market-manager-gui
compression: maximum
productName: 龙岗区百千万AI智能体共创平台
directories:

View File

@@ -1,4 +1,4 @@
appId: com.huashiai.app
appId: com.huashiai.dify-market-manager-gui
compression: maximum
productName: 龙岗区百千万AI智能体共创平台
directories:

73
force-kill-processes.js Normal file
View 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
View 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)

View File

@@ -1,5 +1,5 @@
{
"name": "market-manager-gui",
"name": "dify-market-manager-gui",
"version": "1.0.0",
"description": "百千万AI智能体共创平台",
"main": "./out/main/index.js",

View File

@@ -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 () => {

View File

@@ -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确保窗口销毁完成
}
}
])

View File

@@ -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

154
test-exit-function.js Normal file
View 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()
}
})
}

View 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()
}
})
}