mirror of
https://github.com/dream-pep/koring-launcher.git
synced 2026-09-12 05:45:18 +08:00
221 lines
6.4 KiB
TypeScript
221 lines
6.4 KiB
TypeScript
import electron from 'electron';
|
|||
|
|
import path from 'path';
|
||
|
|
import fs from 'fs';
|
||
|
|
import { writeCrashLog, readCrashLog, clearCrashLog, type CrashEntry } from '../core/crash-logger';
|
||
|
|
|
||
|
|
const { app, ipcMain, BrowserWindow } = electron;
|
||
|
|
|
||
|
|
const isDev = !app.isPackaged;
|
||
|
|
|
||
|
|
let crashWin: electron.BrowserWindow | null = null;
|
||
|
|
|
||
|
|
function getIconPath(): string {
|
||
|
|
return path.join(__dirname, '../../build/icon.ico');
|
||
|
|
}
|
||
|
|
|
||
|
|
function createCrashWindow(): electron.BrowserWindow {
|
||
|
|
const iconPath = getIconPath();
|
||
|
|
|
||
|
|
const win = new BrowserWindow({
|
||
|
|
width: 600,
|
||
|
|
height: 460,
|
||
|
|
minWidth: 500,
|
||
|
|
maxWidth: 700,
|
||
|
|
minHeight: 460,
|
||
|
|
maxHeight: 460,
|
||
|
|
frame: false,
|
||
|
|
transparent: false,
|
||
|
|
resizable: false,
|
||
|
|
show: false,
|
||
|
|
icon: iconPath,
|
||
|
|
webPreferences: {
|
||
|
|
preload: path.join(__dirname, '../preload-crash.js'),
|
||
|
|
nodeIntegration: false,
|
||
|
|
contextIsolation: true,
|
||
|
|
sandbox: false,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
if (isDev) {
|
||
|
|
win.loadURL('http://localhost:1420/crash.html');
|
||
|
|
} else {
|
||
|
|
win.loadFile(path.join(__dirname, '../../dist/crash.html'));
|
||
|
|
}
|
||
|
|
|
||
|
|
return win;
|
||
|
|
}
|
||
|
|
|
||
|
|
function sendToCrashWindow(data: CrashEntry) {
|
||
|
|
if (!crashWin || crashWin.isDestroyed()) {
|
||
|
|
crashWin = createCrashWindow();
|
||
|
|
}
|
||
|
|
|
||
|
|
const send = () => {
|
||
|
|
crashWin?.show();
|
||
|
|
crashWin?.focus();
|
||
|
|
crashWin?.webContents.send('crash:show', {
|
||
|
|
type: data.type,
|
||
|
|
message: data.message,
|
||
|
|
timestamp: data.timestamp,
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
if (crashWin.webContents.isLoading()) {
|
||
|
|
crashWin.webContents.once('did-finish-load', send);
|
||
|
|
} else {
|
||
|
|
send();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function setupCrashListeners(mainWindow: electron.BrowserWindow) {
|
||
|
|
mainWindow.webContents.on('render-process-gone', (_event: any, details: any) => {
|
||
|
|
const entry: CrashEntry = {
|
||
|
|
timestamp: new Date().toISOString(),
|
||
|
|
type: 'renderer-gone',
|
||
|
|
message: `渲染进程崩溃: ${details.reason} (退出码: ${details.exitCode})`,
|
||
|
|
details: details as unknown as Record<string, unknown>,
|
||
|
|
};
|
||
|
|
writeCrashLog(entry);
|
||
|
|
sendToCrashWindow(entry);
|
||
|
|
});
|
||
|
|
|
||
|
|
mainWindow.on('unresponsive', () => {
|
||
|
|
const entry: CrashEntry = {
|
||
|
|
timestamp: new Date().toISOString(),
|
||
|
|
type: 'unresponsive',
|
||
|
|
message: '渲染进程无响应',
|
||
|
|
};
|
||
|
|
writeCrashLog(entry);
|
||
|
|
sendToCrashWindow(entry);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Inject devtools crash tools when devtools opens
|
||
|
|
if (isDev) {
|
||
|
|
mainWindow.webContents.on('devtools-opened', () => {
|
||
|
|
const js = `
|
||
|
|
(function() {
|
||
|
|
if (window.__crashToolsLoaded) return;
|
||
|
|
window.__crashToolsLoaded = true;
|
||
|
|
|
||
|
|
console.log('%c[崩溃工具] 已加载', 'color: #f59e0b; font-weight: bold; font-size: 14px;');
|
||
|
|
console.log('%c可用命令:', 'color: #3b82f6; font-weight: bold;');
|
||
|
|
console.log('%c crash.simulate() %c— 模拟渲染进程崩溃', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
|
||
|
|
console.log('%c crash.testDialog() %c— 测试崩溃弹窗', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
|
||
|
|
console.log('%c crash.readLog() %c— 读取崩溃日志', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
|
||
|
|
console.log('%c crash.factoryReset()%c— 强还原配置', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
|
||
|
|
console.log('%c crash.restart() %c— 重启应用', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
|
||
|
|
console.log('');
|
||
|
|
|
||
|
|
window.crash = {
|
||
|
|
simulate: function() { window.electronAPI?.simulateCrash(); },
|
||
|
|
testDialog: function() { window.electronAPI?.testCrashDialog(); },
|
||
|
|
readLog: function() { return window.electronAPI?.invoke('crash:readLog'); },
|
||
|
|
factoryReset: function() { return window.electronAPI?.invoke('crash:factoryReset'); },
|
||
|
|
restart: function() { return window.electronAPI?.invoke('crash:restart'); },
|
||
|
|
};
|
||
|
|
})();
|
||
|
|
`;
|
||
|
|
mainWindow.webContents.executeJavaScript(js);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function registerCrashHandlers() {
|
||
|
|
// Close crash window
|
||
|
|
ipcMain.handle('crash:closeWindow', () => {
|
||
|
|
if (crashWin && !crashWin.isDestroyed()) {
|
||
|
|
crashWin.close();
|
||
|
|
crashWin = null;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Read crash log
|
||
|
|
ipcMain.handle('crash:readLog', () => {
|
||
|
|
return readCrashLog();
|
||
|
|
});
|
||
|
|
|
||
|
|
// Simulate crash — forcefully crash the renderer (devtools only)
|
||
|
|
ipcMain.handle('crash:simulate', (_event) => {
|
||
|
|
const senderWebContents = electron.webContents.getAllWebContents().find(
|
||
|
|
(wc) => wc.id === _event.sender.id
|
||
|
|
);
|
||
|
|
if (senderWebContents) {
|
||
|
|
senderWebContents.forcefullyCrashRenderer();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Test crash dialog — show crash window without actually crashing
|
||
|
|
ipcMain.handle('crash:testDialog', () => {
|
||
|
|
testCrashDialog();
|
||
|
|
});
|
||
|
|
|
||
|
|
// Factory reset
|
||
|
|
ipcMain.handle('crash:factoryReset', () => {
|
||
|
|
const dataPath = app.isPackaged
|
||
|
|
? path.dirname(app.getPath('exe'))
|
||
|
|
: path.join(__dirname, '../..');
|
||
|
|
|
||
|
|
// Delete config
|
||
|
|
try {
|
||
|
|
const configPath = path.join(dataPath, 'Koring.yml');
|
||
|
|
if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
|
||
|
|
} catch {}
|
||
|
|
|
||
|
|
// Delete auth
|
||
|
|
try {
|
||
|
|
const authPath = path.join(dataPath, 'koring-auth.json');
|
||
|
|
if (fs.existsSync(authPath)) fs.unlinkSync(authPath);
|
||
|
|
} catch {}
|
||
|
|
|
||
|
|
// Delete background cache in userData
|
||
|
|
try {
|
||
|
|
const bgPath = path.join(app.getPath('userData'), 'background.png');
|
||
|
|
if (fs.existsSync(bgPath)) fs.unlinkSync(bgPath);
|
||
|
|
} catch {}
|
||
|
|
|
||
|
|
// Delete crash log
|
||
|
|
clearCrashLog();
|
||
|
|
|
||
|
|
return { success: true };
|
||
|
|
});
|
||
|
|
|
||
|
|
// Restart app
|
||
|
|
ipcMain.handle('crash:restart', () => {
|
||
|
|
app.relaunch();
|
||
|
|
app.exit(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Main process error handlers
|
||
|
|
process.on('uncaughtException', (error) => {
|
||
|
|
const entry: CrashEntry = {
|
||
|
|
timestamp: new Date().toISOString(),
|
||
|
|
type: 'uncaught-exception',
|
||
|
|
message: error.message,
|
||
|
|
stack: error.stack,
|
||
|
|
};
|
||
|
|
writeCrashLog(entry);
|
||
|
|
sendToCrashWindow(entry);
|
||
|
|
});
|
||
|
|
|
||
|
|
process.on('unhandledRejection', (reason) => {
|
||
|
|
const entry: CrashEntry = {
|
||
|
|
timestamp: new Date().toISOString(),
|
||
|
|
type: 'unhandled-rejection',
|
||
|
|
message: String(reason),
|
||
|
|
stack: reason instanceof Error ? reason.stack : undefined,
|
||
|
|
};
|
||
|
|
writeCrashLog(entry);
|
||
|
|
sendToCrashWindow(entry);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Called from devtools to test crash dialog
|
||
|
|
export function testCrashDialog() {
|
||
|
|
const entry: CrashEntry = {
|
||
|
|
timestamp: new Date().toISOString(),
|
||
|
|
type: 'test',
|
||
|
|
message: '这是一个测试崩溃弹窗',
|
||
|
|
};
|
||
|
|
sendToCrashWindow(entry);
|
||
|
|
}
|