mirror of
https://github.com/dream-pep/koring-launcher.git
synced 2026-09-12 05:45:18 +08:00
Add crash monitor, OOBE flow & factory-reset
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.
This commit is contained in:
+11
-1
@@ -7,7 +7,7 @@ const { app } = electron;
|
||||
const CONFIG_FILE = 'Koring.yml';
|
||||
const CURRENT_VERSION = 1;
|
||||
|
||||
function configPath(): string {
|
||||
export function configPath(): string {
|
||||
if (app.isPackaged) {
|
||||
return path.join(path.dirname(app.getPath('exe')), CONFIG_FILE);
|
||||
}
|
||||
@@ -76,6 +76,7 @@ export interface NetworkConfig {
|
||||
|
||||
export interface AppConfig {
|
||||
version: number;
|
||||
oobe: boolean;
|
||||
theme: ThemeConfig;
|
||||
a11y: A11yConfig;
|
||||
background: BackgroundConfig;
|
||||
@@ -88,6 +89,7 @@ export interface AppConfig {
|
||||
|
||||
const DEFAULTS: AppConfig = {
|
||||
version: CURRENT_VERSION,
|
||||
oobe: true,
|
||||
theme: { darkMode: 'auto', parallax: true },
|
||||
a11y: { reduceMotion: false, reduceTransparency: false, highContrast: false, contentBlurOpacity: 50 },
|
||||
background: { bgType: 'image', image: '/background.png', blur: 0, opacity: 100 },
|
||||
@@ -129,6 +131,14 @@ function diffValue(full: unknown, defaultVal: unknown): unknown {
|
||||
return Object.keys(result).length === 0 ? undefined : result;
|
||||
}
|
||||
|
||||
export function configExists(): boolean {
|
||||
try {
|
||||
return fs.existsSync(configPath());
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function loadConfig(): AppConfig {
|
||||
const filePath = configPath();
|
||||
if (!fs.existsSync(filePath)) {
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.writeCrashLog = writeCrashLog;
|
||||
exports.readCrashLog = readCrashLog;
|
||||
exports.clearCrashLog = clearCrashLog;
|
||||
const fs = __importStar(require("fs"));
|
||||
const path = __importStar(require("path"));
|
||||
const electron_1 = __importDefault(require("electron"));
|
||||
const { app } = electron_1.default;
|
||||
const LOG_FILE = 'koring-crash.log';
|
||||
const MAX_LINES = 1000;
|
||||
function logPath() {
|
||||
if (app.isPackaged) {
|
||||
return path.join(path.dirname(app.getPath('exe')), LOG_FILE);
|
||||
}
|
||||
return path.join(__dirname, '..', LOG_FILE);
|
||||
}
|
||||
function trimFile(filePath) {
|
||||
try {
|
||||
if (!fs.existsSync(filePath))
|
||||
return;
|
||||
const lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean);
|
||||
if (lines.length > MAX_LINES) {
|
||||
fs.writeFileSync(filePath, lines.slice(-MAX_LINES).join('\n') + '\n', 'utf-8');
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
function writeCrashLog(entry) {
|
||||
const filePath = logPath();
|
||||
try {
|
||||
const line = JSON.stringify(entry) + '\n';
|
||||
fs.appendFileSync(filePath, line, 'utf-8');
|
||||
trimFile(filePath);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
function readCrashLog() {
|
||||
const filePath = logPath();
|
||||
try {
|
||||
if (!fs.existsSync(filePath))
|
||||
return '';
|
||||
return fs.readFileSync(filePath, 'utf-8');
|
||||
}
|
||||
catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
function clearCrashLog() {
|
||||
const filePath = logPath();
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.writeFileSync(filePath, '', 'utf-8');
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import electron from 'electron';
|
||||
const { app } = electron;
|
||||
|
||||
const LOG_FILE = 'koring-crash.log';
|
||||
const MAX_LINES = 1000;
|
||||
|
||||
function logPath(): string {
|
||||
if (app.isPackaged) {
|
||||
return path.join(path.dirname(app.getPath('exe')), LOG_FILE);
|
||||
}
|
||||
return path.join(__dirname, '..', LOG_FILE);
|
||||
}
|
||||
|
||||
export interface CrashEntry {
|
||||
timestamp: string;
|
||||
type: 'renderer-gone' | 'unresponsive' | 'uncaught-exception' | 'unhandled-rejection' | 'child-process-gone' | 'test';
|
||||
message: string;
|
||||
stack?: string;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
function trimFile(filePath: string): void {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) return;
|
||||
const lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean);
|
||||
if (lines.length > MAX_LINES) {
|
||||
fs.writeFileSync(filePath, lines.slice(-MAX_LINES).join('\n') + '\n', 'utf-8');
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export function writeCrashLog(entry: CrashEntry): void {
|
||||
const filePath = logPath();
|
||||
try {
|
||||
const line = JSON.stringify(entry) + '\n';
|
||||
fs.appendFileSync(filePath, line, 'utf-8');
|
||||
trimFile(filePath);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export function readCrashLog(): string {
|
||||
const filePath = logPath();
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) return '';
|
||||
return fs.readFileSync(filePath, 'utf-8');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function clearCrashLog(): void {
|
||||
const filePath = logPath();
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.writeFileSync(filePath, '', 'utf-8');
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
@@ -1,11 +1,45 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.registerConfigHandlers = registerConfigHandlers;
|
||||
const electron_1 = __importDefault(require("electron"));
|
||||
const { ipcMain } = electron_1.default;
|
||||
const { ipcMain, app } = electron_1.default;
|
||||
const fs = __importStar(require("fs"));
|
||||
const config_1 = require("../config");
|
||||
function registerConfigHandlers() {
|
||||
ipcMain.handle('config:get', () => {
|
||||
@@ -26,4 +60,18 @@ function registerConfigHandlers() {
|
||||
return { success: false, data: null, error: String(e) };
|
||||
}
|
||||
});
|
||||
ipcMain.handle('config:reset', () => {
|
||||
try {
|
||||
const filePath = (0, config_1.configPath)();
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
app.relaunch();
|
||||
app.exit(0);
|
||||
return { success: true, data: null, error: null };
|
||||
}
|
||||
catch (e) {
|
||||
return { success: false, data: null, error: String(e) };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import electron from 'electron';
|
||||
const { ipcMain } = electron;
|
||||
import { loadConfig, saveConfig, type AppConfig } from '../config';
|
||||
const { ipcMain, app } = electron;
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { loadConfig, saveConfig, type AppConfig, configPath } from '../config';
|
||||
|
||||
export function registerConfigHandlers() {
|
||||
ipcMain.handle('config:get', () => {
|
||||
@@ -20,4 +22,18 @@ export function registerConfigHandlers() {
|
||||
return { success: false, data: null, error: String(e) };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('config:reset', () => {
|
||||
try {
|
||||
const filePath = configPath();
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
app.relaunch();
|
||||
app.exit(0);
|
||||
return { success: true, data: null, error: null };
|
||||
} catch (e: unknown) {
|
||||
return { success: false, data: null, error: String(e) };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.setupCrashListeners = setupCrashListeners;
|
||||
exports.registerCrashHandlers = registerCrashHandlers;
|
||||
exports.testCrashDialog = testCrashDialog;
|
||||
const electron_1 = __importDefault(require("electron"));
|
||||
const path_1 = __importDefault(require("path"));
|
||||
const fs_1 = __importDefault(require("fs"));
|
||||
const crash_logger_1 = require("../core/crash-logger");
|
||||
const { app, ipcMain, BrowserWindow } = electron_1.default;
|
||||
const isDev = !app.isPackaged;
|
||||
let crashWin = null;
|
||||
function getIconPath() {
|
||||
return path_1.default.join(__dirname, '../../build/icon.ico');
|
||||
}
|
||||
function createCrashWindow() {
|
||||
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_1.default.join(__dirname, '../preload-crash.js'),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
sandbox: false,
|
||||
},
|
||||
});
|
||||
if (isDev) {
|
||||
win.loadURL('http://localhost:1420/crash.html');
|
||||
}
|
||||
else {
|
||||
win.loadFile(path_1.default.join(__dirname, '../../dist/crash.html'));
|
||||
}
|
||||
return win;
|
||||
}
|
||||
function sendToCrashWindow(data) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
function setupCrashListeners(mainWindow) {
|
||||
mainWindow.webContents.on('render-process-gone', (_event, details) => {
|
||||
const entry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'renderer-gone',
|
||||
message: `渲染进程崩溃: ${details.reason} (退出码: ${details.exitCode})`,
|
||||
details: details,
|
||||
};
|
||||
(0, crash_logger_1.writeCrashLog)(entry);
|
||||
sendToCrashWindow(entry);
|
||||
});
|
||||
mainWindow.on('unresponsive', () => {
|
||||
const entry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'unresponsive',
|
||||
message: '渲染进程无响应',
|
||||
};
|
||||
(0, crash_logger_1.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);
|
||||
});
|
||||
}
|
||||
}
|
||||
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 (0, crash_logger_1.readCrashLog)();
|
||||
});
|
||||
// Simulate crash — forcefully crash the renderer (devtools only)
|
||||
ipcMain.handle('crash:simulate', (_event) => {
|
||||
const senderWebContents = electron_1.default.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_1.default.dirname(app.getPath('exe'))
|
||||
: path_1.default.join(__dirname, '../..');
|
||||
// Delete config
|
||||
try {
|
||||
const configPath = path_1.default.join(dataPath, 'Koring.yml');
|
||||
if (fs_1.default.existsSync(configPath))
|
||||
fs_1.default.unlinkSync(configPath);
|
||||
}
|
||||
catch { }
|
||||
// Delete auth
|
||||
try {
|
||||
const authPath = path_1.default.join(dataPath, 'koring-auth.json');
|
||||
if (fs_1.default.existsSync(authPath))
|
||||
fs_1.default.unlinkSync(authPath);
|
||||
}
|
||||
catch { }
|
||||
// Delete background cache in userData
|
||||
try {
|
||||
const bgPath = path_1.default.join(app.getPath('userData'), 'background.png');
|
||||
if (fs_1.default.existsSync(bgPath))
|
||||
fs_1.default.unlinkSync(bgPath);
|
||||
}
|
||||
catch { }
|
||||
// Delete crash log
|
||||
(0, crash_logger_1.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 = {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'uncaught-exception',
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
};
|
||||
(0, crash_logger_1.writeCrashLog)(entry);
|
||||
sendToCrashWindow(entry);
|
||||
});
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
const entry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'unhandled-rejection',
|
||||
message: String(reason),
|
||||
stack: reason instanceof Error ? reason.stack : undefined,
|
||||
};
|
||||
(0, crash_logger_1.writeCrashLog)(entry);
|
||||
sendToCrashWindow(entry);
|
||||
});
|
||||
}
|
||||
// Called from devtools to test crash dialog
|
||||
function testCrashDialog() {
|
||||
const entry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'test',
|
||||
message: '这是一个测试崩溃弹窗',
|
||||
};
|
||||
sendToCrashWindow(entry);
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
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);
|
||||
}
|
||||
+45
-2
@@ -1,5 +1,6 @@
|
||||
import electron from 'electron';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { registerConfigHandlers } from './handlers/config';
|
||||
import { registerAuthHandlers } from './handlers/auth';
|
||||
import { registerInstallHandlers } from './handlers/install';
|
||||
@@ -10,17 +11,47 @@ import { registerBackgroundHandlers } from './handlers/background';
|
||||
import { registerTaskHandlers } from './handlers/task';
|
||||
import { registerSystemHandlers } from './handlers/system';
|
||||
import { registerWindowHandlers } from './handlers/window';
|
||||
import { registerCrashHandlers, setupCrashListeners, testCrashDialog } from './handlers/crash-monitor';
|
||||
import { loadConfig, saveConfig, configExists } from './config';
|
||||
|
||||
const { app } = electron;
|
||||
|
||||
const isDev = !app.isPackaged;
|
||||
|
||||
// Mutable ref — handlers always read from this
|
||||
// GPU acceleration flags
|
||||
app.commandLine.appendSwitch('enable-gpu-rasterization');
|
||||
app.commandLine.appendSwitch('enable-zero-copy');
|
||||
|
||||
const win: { mainWindow: electron.BrowserWindow | null; splashWindow: electron.BrowserWindow | null } = {
|
||||
mainWindow: null,
|
||||
splashWindow: null,
|
||||
};
|
||||
|
||||
// Startup checks: .minecraft dir + config file + first launch detection
|
||||
function runStartupChecks(): { isFirstLaunch: boolean; config: ReturnType<typeof loadConfig> } {
|
||||
const dataPath = app.isPackaged
|
||||
? path.dirname(app.getPath('exe'))
|
||||
: path.join(__dirname, '..');
|
||||
|
||||
// 1. Ensure .minecraft directory exists
|
||||
const minecraftDir = path.join(dataPath, '.minecraft');
|
||||
if (!fs.existsSync(minecraftDir)) {
|
||||
fs.mkdirSync(minecraftDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 2. Check if config exists, if not create with defaults
|
||||
const hasConfig = configExists();
|
||||
const isFirstLaunch = !hasConfig;
|
||||
|
||||
// 3. Load (or create) config
|
||||
const config = loadConfig();
|
||||
if (isFirstLaunch) {
|
||||
saveConfig(config);
|
||||
}
|
||||
|
||||
return { isFirstLaunch, config };
|
||||
}
|
||||
|
||||
function createSplashWindow(): electron.BrowserWindow {
|
||||
const iconPath = isDev
|
||||
? path.join(__dirname, '../build/icon.ico')
|
||||
@@ -101,18 +132,27 @@ function registerAllHandlers() {
|
||||
registerTaskHandlers(win);
|
||||
registerSystemHandlers();
|
||||
registerWindowHandlers(win);
|
||||
registerCrashHandlers();
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
registerAllHandlers();
|
||||
|
||||
// Run startup checks before creating windows
|
||||
const { isFirstLaunch, config } = runStartupChecks();
|
||||
|
||||
// 1. Show splash immediately
|
||||
win.splashWindow = createSplashWindow();
|
||||
|
||||
// 2. Create main window in background
|
||||
win.mainWindow = createMainWindow();
|
||||
|
||||
// 3. When main window finishes loading, wait a minimum time then transition
|
||||
// 3. Preload config into renderer before it renders
|
||||
win.mainWindow.webContents.on('did-finish-load', () => {
|
||||
win.mainWindow?.webContents.send('config:preload', { config, isFirstLaunch });
|
||||
});
|
||||
|
||||
// 4. When main window finishes loading, wait a minimum time then transition
|
||||
let mainReady = false;
|
||||
let splashMinTimeDone = false;
|
||||
|
||||
@@ -134,6 +174,9 @@ app.whenReady().then(() => {
|
||||
tryTransition();
|
||||
});
|
||||
|
||||
// Setup crash listeners on main window
|
||||
setupCrashListeners(win.mainWindow);
|
||||
|
||||
// Minimum splash display time (1.5s)
|
||||
setTimeout(() => {
|
||||
splashMinTimeDone = true;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import electron from 'electron';
|
||||
const { contextBridge, ipcRenderer } = electron;
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
invoke: (channel: string, ...args: unknown[]) =>
|
||||
ipcRenderer.invoke(channel, ...args),
|
||||
|
||||
on: (channel: string, callback: (...args: unknown[]) => void) => {
|
||||
const handler = (_event: Electron.IpcRendererEvent, ...args: unknown[]) => callback(...args);
|
||||
ipcRenderer.on(channel, handler);
|
||||
return () => ipcRenderer.removeListener(channel, handler);
|
||||
},
|
||||
|
||||
send: (channel: string, ...args: unknown[]) =>
|
||||
ipcRenderer.send(channel, ...args),
|
||||
|
||||
minimize: () => ipcRenderer.invoke('window:minimize'),
|
||||
maximize: () => ipcRenderer.invoke('window:maximize'),
|
||||
close: () => ipcRenderer.invoke('crash:closeWindow'),
|
||||
isMaximized: () => ipcRenderer.invoke('window:isMaximized'),
|
||||
onResized: (callback: () => void) => {
|
||||
const handler = () => callback();
|
||||
ipcRenderer.on('window:resized', handler);
|
||||
return () => ipcRenderer.removeListener('window:resized', handler);
|
||||
},
|
||||
|
||||
getTheme: () => ipcRenderer.invoke('window:getTheme'),
|
||||
openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url),
|
||||
});
|
||||
@@ -51,6 +51,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// Theme
|
||||
getTheme: () => ipcRenderer.invoke('window:getTheme'),
|
||||
|
||||
// Config preloading
|
||||
onConfigPreload: (callback: (data: { config: unknown; isFirstLaunch: boolean }) => void) => {
|
||||
const handler = (_event: Electron.IpcRendererEvent, data: { config: unknown; isFirstLaunch: boolean }) => callback(data);
|
||||
ipcRenderer.on('config:preload', handler);
|
||||
return () => ipcRenderer.removeListener('config:preload', handler);
|
||||
},
|
||||
|
||||
// Background image — pick file, copy to userData, return base64 data URL
|
||||
pickBackgroundImage: async (): Promise<string | null> => {
|
||||
const result = await ipcRenderer.invoke('dialog:openFile', {
|
||||
@@ -73,4 +80,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
|
||||
// Open external URL in system browser
|
||||
openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url),
|
||||
|
||||
// Crash monitoring — devtools only
|
||||
simulateCrash: () => ipcRenderer.invoke('crash:simulate'),
|
||||
testCrashDialog: () => ipcRenderer.invoke('crash:testDialog'),
|
||||
|
||||
// Config reset
|
||||
resetConfig: () => ipcRenderer.invoke('config:reset'),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user