mirror of
https://github.com/dream-pep/koring-launcher.git
synced 2026-09-12 05:45:18 +08:00
Introduce crash monitoring and an OOBE onboarding flow. Adds a crash logger (koring-crash.log), crash-monitor handler/window, preload-crash bridge, crash UI (crash.html, src/crash.tsx, pages/crash) and a debug page. Main process now sets up crash listeners, startup checks (first-launch, .minecraft), and a config reset/factory-reset that removes Koring.yml, koring-auth.json and background cache and can relaunch the app. Exposes config preloading to renderer, adds OOBE pages/layout, confirm dialog store/UI, trims Silk renderer settings, bumps version to 1.0.1 and adds motion dependency, and registers crash.html in Vite. Various related type and config updates included.
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);
|
|
}
|