[eric] windows: serve packaged frontend via embedded http://127.0.0.1 server instead of file:// since dev mode (http origin) never crashed while packaged (file://) consistently 0xC0000005 segfaulted on chat/dashboard mount, confirming file:// origin is the actual native-crash trigger on Electron 40 CastLabs Chromium 144; revert disableHardwareAcceleration to keep GPU perf since the fault was never GPU-side; retag v1.1.44

This commit is contained in:
Eric
2026-05-25 18:40:06 -07:00
parent 9a6956477f
commit 0bec1ea268
+80 -4
View File
@@ -153,10 +153,7 @@ app.on('open-url', (event, url) => {
});
app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling');
// Disable hardware acceleration entirely on Windows. Removing the override flags alone did not stop the 0xC0000005 (ACCESS_VIOLATION) renderer crashes that Crashpad captured on every chat / dashboard mount, so the native segfault is not just blocklist-bypass GPU work, it is somewhere in the GPU process itself. Software rasterization is slower but eliminates the entire class of GPU-side native crashes. We can scope this to win32 only since macOS users have never reported the issue.
if (process.platform === 'win32') {
app.disableHardwareAcceleration();
}
// disableHardwareAcceleration() was tried as a fallback but did not stop the 0xC0000005 crashes, confirming the segfault is not GPU-side. Dev mode (http origin) never crashed, packaged (file:// origin) always crashed, so the embedded localhost HTTP server (see startFrontendServer below) is the real fix and we keep GPU acceleration on.
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required');
let mainWindow = null;
@@ -177,6 +174,74 @@ let splashDataUrlCache = null;
// Set to true around `new BrowserWindow()` for the top-level main window so the popup-UA spoofer in app.on('web-contents-created') doesn't accidentally rewrite the main window's UA. The web-contents-created event fires synchronously inside the BrowserWindow constructor, before mainWindow assignment returns; without this flag, the previous identity check (contents !== mainWindow.webContents) is racy across recreateMainWindow() because mainWindow still points to the OLD window during construction of the NEW one.
let isCreatingMainWindow = false;
// Embedded HTTP server that serves the packaged frontend bundle. The previous loadFile(...) path used file:// which on Windows Electron 40 CastLabs triggered a STATUS_ACCESS_VIOLATION (0xC0000005) renderer crash on every chat / dashboard mount; dev mode using http://localhost:3000 never crashed. Serving over http://127.0.0.1:<random> from the same in-process Node http server keeps the same packaged asset layout, costs no measurable perf (in-process loopback), and avoids the file:// quirk that Chromium 144 segfaults on.
let frontendServerPort = null;
async function startFrontendServer() {
const frontendDir = path.join(process.resourcesPath, 'frontend');
const mimeTypes = {
'.html': 'text/html; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.mjs': 'application/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.map': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.otf': 'font/otf',
'.mp4': 'video/mp4',
'.webm': 'video/webm',
'.wasm': 'application/wasm',
};
const server = http.createServer((req, res) => {
try {
let pathname = decodeURIComponent((req.url || '/').split('?')[0]);
if (pathname === '/' || pathname === '') pathname = '/index.html';
const resolved = path.normalize(path.join(frontendDir, pathname));
// Defense-in-depth path-traversal guard; loopback-only listener already prevents external access but a misparsed URL must not escape the frontend dir.
if (!resolved.startsWith(frontendDir + path.sep) && resolved !== path.join(frontendDir, 'index.html')) {
res.writeHead(403); res.end(); return;
}
fs.readFile(resolved, (err, data) => {
if (err) {
// SPA fallback: unknown paths return index.html so client-side routing works even if some code uses BrowserRouter instead of HashRouter.
fs.readFile(path.join(frontendDir, 'index.html'), (err2, indexData) => {
if (err2) { res.writeHead(404); res.end(); return; }
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(indexData);
});
return;
}
const ext = path.extname(resolved).toLowerCase();
res.writeHead(200, { 'Content-Type': mimeTypes[ext] || 'application/octet-stream' });
res.end(data);
});
} catch (err) {
console.error('[frontend-server] request handler threw:', err && err.message);
try { res.writeHead(500); res.end(); } catch (_) {}
}
});
return new Promise((resolve, reject) => {
server.on('error', (err) => {
console.error('[frontend-server] failed to start:', err && err.message);
reject(err);
});
server.listen(0, '127.0.0.1', () => {
const addr = server.address();
frontendServerPort = typeof addr === 'object' && addr ? addr.port : null;
console.log(`[frontend-server] listening on 127.0.0.1:${frontendServerPort}`);
resolve(frontendServerPort);
});
});
}
const isPackaged = app.isPackaged;
const isDev = process.env.ELECTRON_DEV === '1';
const iconPath = process.platform === 'win32'
@@ -728,7 +793,10 @@ function createWindow() {
if (isDev) {
mainWindow.loadURL(`http://localhost:3000`);
} else if (frontendServerPort) {
mainWindow.loadURL(`http://127.0.0.1:${frontendServerPort}/index.html`);
} else {
// Fallback only if the embedded server failed to start; the file:// path is known to segfault on Windows CastLabs Electron 40 but it is better than a white screen.
const frontendPath = getResourcePath('frontend', 'index.html');
mainWindow.loadFile(frontendPath);
}
@@ -1106,6 +1174,14 @@ app.whenReady().then(async () => {
emitSplashStatus({ text: 'Backend failed to start', level: 'error', logs: recentBackendStderr.slice(-20).join('') });
});
}
// Start the embedded frontend HTTP server before createWindow so loadURL has a real port. Only relevant in packaged mode; in dev, frontend lives on webpack-dev-server :3000.
if (!isDev) {
try {
await startFrontendServer();
} catch (err) {
console.error('[boot] frontend server failed to start, falling back to file://:', err && err.message);
}
}
emitSplashStatus('Almost ready…');
createWindow();
if (!isDev) {