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

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