初始化
This commit is contained in:
228
main.js
Normal file
228
main.js
Normal file
@@ -0,0 +1,228 @@
|
||||
// main.js
|
||||
const { app, Tray, Menu, dialog, BrowserWindow, ipcMain, nativeTheme } = require('electron');
|
||||
const { autoUpdater } = require('electron-updater');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { spawn, spawnSync, exec } = require('child_process');
|
||||
const log = require('electron-log');
|
||||
|
||||
Object.assign(console, log.functions);
|
||||
|
||||
const ALLOW_UNSIGNED = process.env.YOLO_ALLOW_UNSIGNED === '1';
|
||||
let installTriggered = false; // NEW: 防止重复触发安装
|
||||
|
||||
// ===== 全局状态 =====
|
||||
let tray = null;
|
||||
let managedChild = null;
|
||||
let quitting = false;
|
||||
let updateWin = null;
|
||||
let pythonStarted = false; // NEW: 已启动 tkpage?
|
||||
let updaterKicked = false; // NEW: 是否已触发检查
|
||||
|
||||
// ===== tkpage 路径 =====
|
||||
function resolveExePath() {
|
||||
const devExe = path.resolve(__dirname, 'resources', 'python', 'tkpage.exe');
|
||||
const prodExe = path.join(process.resourcesPath, 'python', 'tkpage.exe');
|
||||
return app.isPackaged ? prodExe : devExe;
|
||||
}
|
||||
|
||||
// ===== 启动 tkpage(只会启动一次) =====
|
||||
function launchPythonIfNeeded() {
|
||||
if (pythonStarted) return;
|
||||
const exe = resolveExePath();
|
||||
const cwd = path.dirname(exe);
|
||||
|
||||
console.info('[tkpage exe]', exe);
|
||||
if (!fs.existsSync(exe)) {
|
||||
dialog.showErrorBox('启动失败', `未找到可执行文件:\n${exe}`);
|
||||
return;
|
||||
}
|
||||
|
||||
pythonStarted = true;
|
||||
managedChild = spawn(exe, [], {
|
||||
cwd,
|
||||
windowsHide: false,
|
||||
detached: false,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
|
||||
managedChild.on('exit', (code, signal) => {
|
||||
console.info(`[tkpage] exit code=${code} signal=${signal}`);
|
||||
if (!quitting) { quitting = true; app.quit(); }
|
||||
});
|
||||
managedChild.on('close', (code, signal) => {
|
||||
console.info(`[tkpage] close code=${code} signal=${signal}`);
|
||||
if (!quitting) { quitting = true; app.quit(); }
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 结束 tkpage =====
|
||||
function killPythonGUI(killSelf = true) {
|
||||
if (managedChild && managedChild.pid) {
|
||||
try { process.kill(managedChild.pid); } catch (e) {
|
||||
console.warn('[kill] managed kill error:', e?.message || e);
|
||||
} finally { managedChild = null; }
|
||||
}
|
||||
try { spawnSync('taskkill', ['/f', '/t', '/im', 'tkpage.exe'], { windowsHide: true }); } catch { }
|
||||
|
||||
if (killSelf) {
|
||||
try { exec('taskkill /IM YOLO助手.exe /F'); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 托盘 =====
|
||||
function createTray() {
|
||||
const iconPath = path.join(__dirname, 'assets', 'icon.ico');
|
||||
tray = new Tray(iconPath);
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{ label: '检查更新', click: () => startUpdateCheck(/*force*/true) },
|
||||
{ type: 'separator' },
|
||||
{ label: '显示更新窗口', click: () => { if (updateWin) { updateWin.show(); updateWin.focus(); } else { createUpdateWindow(); } } },
|
||||
{ type: 'separator' },
|
||||
{ label: '重启 Python GUI', click: () => { killPythonGUI(); pythonStarted = false; launchPythonIfNeeded(); } },
|
||||
{ type: 'separator' },
|
||||
{ label: '退出', click: () => { quitting = true; killPythonGUI(); app.quit(); } },
|
||||
]);
|
||||
tray.setToolTip('YOLO');
|
||||
tray.setContextMenu(menu);
|
||||
}
|
||||
|
||||
// ===== 更新窗口 =====
|
||||
function createUpdateWindow() {
|
||||
if (updateWin && !updateWin.isDestroyed()) { updateWin.show(); updateWin.focus(); return; }
|
||||
updateWin = new BrowserWindow({
|
||||
width: 640, height: 420, resizable: false, fullscreenable: false, maximizable: false,
|
||||
autoHideMenuBar: true, show: true, center: true,
|
||||
backgroundColor: nativeTheme.shouldUseDarkColors ? '#0b1220' : '#ffffff',
|
||||
icon: path.join(__dirname, 'assets', 'icon.ico'),
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
contextIsolation: true, nodeIntegration: false, sandbox: true,
|
||||
}
|
||||
});
|
||||
updateWin.on('close', (e) => { e.preventDefault(); updateWin.hide(); });
|
||||
updateWin.loadFile(path.join(__dirname, 'index.html')).catch(err => console.error('[updateWin] load error', err));
|
||||
}
|
||||
|
||||
// ===== Updater 事件桥接到渲染进程,并控制 tkpage 启动时机 =====
|
||||
function wireUpdaterIpc() {
|
||||
if (ALLOW_UNSIGNED) {
|
||||
console.warn('[updater] verifyUpdateCodeSignature disabled (YOLO_ALLOW_UNSIGNED=1)');
|
||||
autoUpdater.verifyUpdateCodeSignature = async () => null;
|
||||
}
|
||||
autoUpdater.autoDownload = true;
|
||||
autoUpdater.autoInstallOnAppQuit = false;
|
||||
|
||||
const send = (ch, data) => { if (updateWin && !updateWin.isDestroyed()) updateWin.webContents.send(ch, data); };
|
||||
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
console.info('[updater] Checking');
|
||||
tray?.setToolTip('YOLO - 正在检查更新…');
|
||||
send('update:status', { status: 'checking' });
|
||||
});
|
||||
|
||||
autoUpdater.on('update-available', (info) => {
|
||||
console.info('[updater] Available', info?.version);
|
||||
tray?.setToolTip(`YOLO - 检测到新版本 v${info?.version},开始下载…`);
|
||||
send('update:status', { status: 'available', version: info?.version, notes: info?.releaseNotes || '' });
|
||||
// 此时不启动 tkpage,等待下载完成
|
||||
});
|
||||
|
||||
autoUpdater.on('update-not-available', (info) => {
|
||||
console.info('[updater] None');
|
||||
tray?.setToolTip('YOLO - 已是最新版本');
|
||||
send('update:status', { status: 'none', currentVersion: app.getVersion(), info });
|
||||
// ✅ 没有更新,立刻启动 tkpage
|
||||
launchPythonIfNeeded();
|
||||
// 可隐藏更新窗口(让应用留在托盘+tkpage前台)
|
||||
setTimeout(() => updateWin?.hide(), 1200);
|
||||
});
|
||||
|
||||
autoUpdater.on('download-progress', (p) => {
|
||||
const percent = Math.max(0, Math.min(100, p?.percent || 0));
|
||||
const transferred = Math.floor((p?.transferred || 0) / 1024 / 1024);
|
||||
const total = Math.floor((p?.total || 0) / 1024 / 1024);
|
||||
const speed = Math.floor((p?.bytesPerSecond || 0) / 1024 / 1024);
|
||||
tray?.setToolTip(`YOLO - 正在下载更新:${percent.toFixed(1)}%`);
|
||||
send('update:progress', { percent, transferred, total, speed });
|
||||
});
|
||||
|
||||
autoUpdater.on('update-downloaded', (info) => {
|
||||
console.info('[updater] Downloaded', info?.version);
|
||||
tray?.setToolTip('YOLO - 更新已下载,等待安装…');
|
||||
// 仅通知渲染进程显示“立即安装”按钮;不自动安装
|
||||
send('update:status', { status: 'downloaded', version: info?.version });
|
||||
// 可选择把更新窗口确保前置
|
||||
if (updateWin && !updateWin.isDestroyed()) { updateWin.show(); updateWin.focus(); }
|
||||
});
|
||||
|
||||
autoUpdater.on('error', (err) => {
|
||||
const msg = err?.message || String(err);
|
||||
console.error('[updater] error', msg);
|
||||
tray?.setToolTip('YOLO - 更新失败');
|
||||
dialog.showErrorBox('自动更新失败', msg);
|
||||
send('update:status', { status: 'error', message: msg });
|
||||
// ⚠️ 出错:不阻断主流程,直接启动 tkpage
|
||||
launchPythonIfNeeded();
|
||||
});
|
||||
|
||||
// 渲染侧手动触发
|
||||
ipcMain.on('updater:check', () => startUpdateCheck(/*force*/true));
|
||||
ipcMain.on('updater:install-now', () => {
|
||||
if (installTriggered) return;
|
||||
installTriggered = true;
|
||||
|
||||
console.info('[updater] quitAndInstall requested by renderer');
|
||||
tray?.setToolTip('YOLO - 正在安装更新…');
|
||||
|
||||
quitting = true;
|
||||
try { killPythonGUI(false); } catch (e) {
|
||||
console.warn('[updater] killPythonGUI error:', e?.message || e);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
autoUpdater.quitAndInstall(false, true);
|
||||
} catch (e) {
|
||||
installTriggered = false;
|
||||
console.error('[updater] quitAndInstall error:', e?.message || e);
|
||||
dialog.showErrorBox('安装失败', String(e?.message || e));
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 触发更新检查(仅触发一次,托盘点“检查更新”可强制再触发) =====
|
||||
function startUpdateCheck(force = false) {
|
||||
if (updaterKicked && !force) return;
|
||||
updaterKicked = true;
|
||||
// 显示更新窗口(可选:你也可以只在有更新时显示)
|
||||
if (!updateWin || updateWin.isDestroyed()) createUpdateWindow();
|
||||
else { updateWin.show(); updateWin.focus(); }
|
||||
autoUpdater.checkForUpdates().catch(() => { /* 错误会在 on('error') 里处理 */ });
|
||||
}
|
||||
|
||||
// ===== 单例 & 生命周期 =====
|
||||
process.on('unhandledRejection', (r) => console.error('[unhandledRejection]', r));
|
||||
process.on('uncaughtException', (e) => console.error('[uncaughtException]', e));
|
||||
|
||||
const gotLock = app.requestSingleInstanceLock();
|
||||
if (!gotLock) {
|
||||
app.quit();
|
||||
} else {
|
||||
app.on('second-instance', () => { if (updateWin) { updateWin.show(); updateWin.focus(); } });
|
||||
|
||||
app.whenReady().then(() => {
|
||||
if (process.platform === 'win32') {
|
||||
app.setAppUserModelId('com.yolozs.newPage');
|
||||
}
|
||||
createTray();
|
||||
createUpdateWindow();
|
||||
wireUpdaterIpc();
|
||||
// ✅ 启动时:只检查更新,不启动 tkpage。无更新 → 启动;有更新 → 下载并重启安装。
|
||||
startUpdateCheck();
|
||||
});
|
||||
|
||||
app.on('before-quit', () => { quitting = true; killPythonGUI(true); });
|
||||
app.on('window-all-closed', () => { /* 后台常驻 */ });
|
||||
}
|
||||
Reference in New Issue
Block a user