feat(electron): add a new crash report HTML page for displaying error details and logs

This commit is contained in:
2026-01-03 15:45:05 -06:00
parent b59c21f483
commit 46f2700770
7 changed files with 20341 additions and 83 deletions

View File

File diff suppressed because one or more lines are too long

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 289 KiB

148
electron/crash.html Normal file
View File

@@ -0,0 +1,148 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;"
/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<title>MeshChatX - Crash Report</title>
<script src="./assets/js/tailwindcss/tailwind-v3.4.3-forms-v0.5.7.js"></script>
</head>
<body class="min-h-screen bg-slate-100 text-gray-900 antialiased dark:bg-zinc-950 dark:text-zinc-50 transition-colors">
<div class="absolute inset-0 -z-10 overflow-hidden">
<div class="absolute -left-32 -top-40 h-80 w-80 rounded-full bg-gradient-to-br from-red-500/30 via-orange-500/20 to-rose-500/30 blur-3xl dark:from-red-600/25 dark:via-orange-600/25 dark:to-rose-600/25"></div>
<div class="absolute -right-24 top-20 h-64 w-64 rounded-full bg-gradient-to-br from-orange-400/30 via-red-500/20 to-rose-500/30 blur-3xl dark:from-orange-500/25 dark:via-red-500/25 dark:to-rose-500/25"></div>
</div>
<main class="relative flex min-h-screen items-center justify-center px-4 py-6 sm:px-6">
<div class="w-full max-w-5xl">
<div class="rounded-2xl border border-slate-200/80 bg-white/80 shadow-2xl backdrop-blur-xl ring-1 ring-white/60 dark:border-zinc-800/70 dark:bg-zinc-900/70 dark:ring-zinc-800/70 transition-colors overflow-hidden">
<div class="p-4 sm:p-6 space-y-4">
<div class="flex flex-col sm:flex-row items-center sm:items-start gap-3 text-center sm:text-left">
<div class="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-red-500 via-orange-500 to-rose-500 shadow-lg ring-4 ring-white/60 dark:ring-zinc-800/70">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div class="space-y-0.5">
<div class="text-xl font-semibold tracking-tight text-gray-900 dark:text-white">
MeshChatX Crashed
</div>
<div class="text-xs text-gray-600 dark:text-gray-300">Critical error detected in backend service.</div>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
<div class="rounded-xl border border-red-200/90 bg-red-50/70 p-3 dark:border-red-900/40 dark:bg-red-900/20 transition-colors">
<div class="text-[10px] uppercase tracking-wide text-red-600 dark:text-red-400 font-semibold">Exit Code</div>
<div class="mt-0.5 text-base font-mono font-bold text-red-700 dark:text-red-300" id="exit-code">--</div>
</div>
<div class="rounded-xl border border-slate-200/90 bg-white/70 p-3 text-center sm:text-right dark:border-zinc-800/80 dark:bg-zinc-900/70 transition-colors">
<div class="text-[10px] uppercase tracking-wide text-gray-500 dark:text-gray-400">Status</div>
<div class="mt-0.5 text-base font-semibold text-red-600 dark:text-red-400">Offline</div>
</div>
</div>
<div class="space-y-3">
<div class="flex flex-col sm:flex-row items-center justify-between gap-2 px-1">
<h3 class="text-[10px] font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Diagnostic Logs</h3>
<button onclick="copyLogs()" class="w-full sm:w-auto text-[10px] font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300 bg-blue-50 dark:bg-blue-900/30 px-3 py-1 rounded-lg transition-colors">
Copy all logs
</button>
</div>
<div class="space-y-1">
<div class="text-[10px] font-medium text-gray-500 dark:text-gray-400 px-1">Standard Output (stdout)</div>
<div class="relative group">
<pre id="stdout" class="h-52 overflow-auto rounded-xl border border-slate-200 bg-slate-50 p-3 font-mono text-[10px] text-slate-700 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-300 select-text scrollbar-thin scrollbar-thumb-slate-300 dark:scrollbar-thumb-zinc-800"></pre>
</div>
</div>
<div class="space-y-1">
<div class="text-[10px] font-medium text-gray-500 dark:text-gray-400 px-1">Standard Error (stderr)</div>
<div class="relative group">
<pre id="stderr" class="h-64 overflow-auto rounded-xl border border-red-100 bg-red-50/50 p-3 font-mono text-[10px] text-red-700 dark:border-red-900/20 dark:bg-zinc-950 dark:text-red-400 select-text scrollbar-thin scrollbar-thumb-red-200 dark:scrollbar-thumb-zinc-800"></pre>
</div>
</div>
</div>
<div class="flex flex-wrap items-center justify-center sm:justify-start gap-2 pt-2">
<button onclick="window.electron.relaunch()" class="w-full sm:w-40 rounded-xl bg-blue-600 px-4 py-2.5 text-xs font-semibold text-white shadow-lg shadow-blue-500/25 hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-900 transition-all active:scale-[0.98]">
Relaunch
</button>
<button onclick="window.electron.relaunchEmergency()" class="w-full sm:w-48 rounded-xl bg-orange-600 px-4 py-2.5 text-xs font-semibold text-white shadow-lg shadow-orange-500/25 hover:bg-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-900 transition-all active:scale-[0.98]">
Engage Emergency Mode
</button>
<button onclick="window.electron.shutdown()" class="w-full sm:w-24 rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-xs font-semibold text-gray-700 shadow-sm hover:bg-slate-50 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800 transition-all active:scale-[0.98]">
Exit
</button>
</div>
</div>
</div>
</div>
</main>
<script>
// Get data from URL parameters
const params = new URLSearchParams(window.location.search);
document.getElementById('exit-code').innerText = params.get('code') || 'Unknown';
// Decoded from base64 to handle complex characters safely
try {
const stdoutBase64 = params.get('stdout') || '';
const stderrBase64 = params.get('stderr') || '';
document.getElementById('stdout').innerText = stdoutBase64 ? atob(stdoutBase64) : 'No output recorded.';
document.getElementById('stderr').innerText = stderrBase64 ? atob(stderrBase64) : 'No error output recorded.';
} catch (e) {
document.getElementById('stdout').innerText = 'Error decoding logs.';
document.getElementById('stderr').innerText = 'Error decoding logs.';
}
function copyLogs() {
const stdout = document.getElementById('stdout').innerText;
const stderr = document.getElementById('stderr').innerText;
const exitCode = document.getElementById('exit-code').innerText;
const fullReport = `MeshChatX Crash Report\nExit Code: ${exitCode}\n\n--- STDOUT ---\n${stdout}\n\n--- STDERR ---\n${stderr}`;
navigator.clipboard.writeText(fullReport).then(() => {
const btn = event.target;
const originalText = btn.innerText;
btn.innerText = 'Copied!';
btn.classList.replace('text-blue-600', 'text-emerald-600');
btn.classList.replace('dark:text-blue-400', 'dark:text-emerald-400');
setTimeout(() => {
btn.innerText = originalText;
btn.classList.replace('text-emerald-600', 'text-blue-600');
btn.classList.replace('dark:text-emerald-400', 'dark:text-blue-400');
}, 2000);
});
}
function detectPreferredTheme() {
try {
const storedTheme =
localStorage.getItem("meshchat.theme") || localStorage.getItem("meshchatx.theme");
if (storedTheme === "dark" || storedTheme === "light") {
return storedTheme;
}
} catch (e) {}
return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
function applyTheme(theme) {
const isDark = theme === "dark";
document.documentElement.classList.toggle("dark", isDark);
}
// Apply theme
applyTheme(detectPreferredTheme());
</script>
</body>
</html>

View File

@@ -1,6 +1,10 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' http://localhost:9337 https://localhost:9337;"
/>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<meta name="color-scheme" content="light dark" />
@@ -19,15 +23,15 @@
></div>
</div>
<main class="relative flex min-h-screen items-center justify-center px-6 py-10">
<main class="relative flex min-h-screen items-center justify-center px-4 py-10 sm:px-6">
<div class="w-full max-w-xl">
<div
class="rounded-3xl border border-slate-200/80 bg-white/80 shadow-2xl backdrop-blur-xl ring-1 ring-white/60 dark:border-zinc-800/70 dark:bg-zinc-900/70 dark:ring-zinc-800/70 transition-colors"
>
<div class="p-8 space-y-6">
<div class="flex items-center gap-4">
<div class="p-6 sm:p-8 space-y-6">
<div class="flex flex-col sm:flex-row items-center sm:items-start gap-4 text-center sm:text-left">
<div
class="flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-blue-500 via-indigo-500 to-purple-500 shadow-lg ring-4 ring-white/60 dark:ring-zinc-800/70"
class="flex h-16 w-16 shrink-0 items-center justify-center rounded-2xl bg-gradient-to-br from-blue-500 via-indigo-500 to-purple-500 shadow-lg ring-4 ring-white/60 dark:ring-zinc-800/70"
>
<img
class="h-10 w-10 object-contain"
@@ -36,9 +40,6 @@
/>
</div>
<div class="space-y-1">
<p class="text-xs uppercase tracking-[0.2em] text-blue-600 dark:text-blue-300">
MeshChatX
</p>
<div class="text-2xl font-semibold tracking-tight text-gray-900 dark:text-white">
MeshChatX
</div>
@@ -47,11 +48,11 @@
</div>
<div
class="flex items-center justify-between rounded-2xl border border-dashed border-slate-200/90 bg-slate-50/70 px-4 py-3 text-sm text-gray-700 dark:border-zinc-800/80 dark:bg-zinc-900/70 dark:text-gray-200 transition-colors"
class="flex flex-col sm:flex-row items-center justify-between gap-3 rounded-2xl border border-dashed border-slate-200/90 bg-slate-50/70 px-4 py-3 text-sm text-gray-700 dark:border-zinc-800/80 dark:bg-zinc-900/70 dark:text-gray-200 transition-colors"
>
<div class="flex items-center gap-2">
<span class="h-2 w-2 rounded-full bg-blue-500 animate-pulse"></span>
<span>Preparing your node</span>
<span>Preparing your app</span>
</div>
<div
class="inline-flex items-center gap-2 rounded-full bg-blue-100/80 px-3 py-1 text-xs font-semibold text-blue-700 shadow-sm dark:bg-blue-900/50 dark:text-blue-200"
@@ -61,8 +62,8 @@
</div>
</div>
<div class="flex items-center gap-4">
<div class="relative inline-flex h-14 w-14 items-center justify-center">
<div class="flex flex-col sm:flex-row items-center sm:items-start gap-4 text-center sm:text-left">
<div class="relative inline-flex h-14 w-14 shrink-0 items-center justify-center">
<span
class="absolute inset-0 rounded-full border-4 border-blue-500/25 dark:border-blue-500/20"
></span>
@@ -79,7 +80,7 @@
</div>
</div>
<div class="grid grid-cols-2 gap-4 text-sm">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
<div
class="rounded-2xl border border-slate-200/90 bg-white/70 p-4 dark:border-zinc-800/80 dark:bg-zinc-900/70 transition-colors"
>

366
electron/main-legacy.js Normal file
View File

@@ -0,0 +1,366 @@
const { app, BrowserWindow, dialog, ipcMain, shell, systemPreferences } = require("electron");
const electronPrompt = require("electron-prompt");
const { spawn } = require("child_process");
const fs = require("fs");
const path = require("node:path");
// remember main window
var mainWindow = null;
// remember child process for exe so we can kill it when app exits
var exeChildProcess = null;
// allow fetching app version via ipc
ipcMain.handle("app-version", () => {
return app.getVersion();
});
// allow fetching hardware acceleration status via ipc
ipcMain.handle("is-hardware-acceleration-enabled", () => {
// New in Electron 39, fallback for legacy
if (typeof app.isHardwareAccelerationEnabled === "function") {
return app.isHardwareAccelerationEnabled();
}
return true; // Assume true for older versions
});
// allow fetching integrity status (Stub for legacy)
ipcMain.handle("get-integrity-status", () => {
return {
backend: { ok: true, issues: ["Not supported in legacy mode"] },
data: { ok: true, issues: ["Not supported in legacy mode"] },
};
});
// ignore ssl errors
app.commandLine.appendSwitch("ignore-certificate-errors");
// add support for showing an alert window via ipc
ipcMain.handle("alert", async (event, message) => {
return await dialog.showMessageBox(mainWindow, {
message: message,
});
});
// add support for showing a confirm window via ipc
ipcMain.handle("confirm", async (event, message) => {
// show confirm dialog
const result = await dialog.showMessageBox(mainWindow, {
type: "question",
title: "Confirm",
message: message,
cancelId: 0, // esc key should press cancel button
defaultId: 1, // enter key should press ok button
buttons: [
"Cancel", // 0
"OK", // 1
],
});
// check if user clicked OK
return result.response === 1;
});
// add support for showing a prompt window via ipc
ipcMain.handle("prompt", async (event, message) => {
return await electronPrompt({
title: message,
label: "",
value: "",
type: "input",
inputAttrs: {
type: "text",
},
});
});
// allow relaunching app via ipc
ipcMain.handle("relaunch", () => {
app.relaunch();
app.exit();
});
// allow showing a file path in os file manager
ipcMain.handle("showPathInFolder", (event, path) => {
shell.showItemInFolder(path);
});
function log(message) {
// log to stdout of this process
console.log(message);
// make sure main window exists
if (!mainWindow) {
return;
}
// make sure window is not destroyed
if (mainWindow.isDestroyed()) {
return;
}
// log to web console
mainWindow.webContents.send("log", message);
}
function getDefaultStorageDir() {
// if we are running a windows portable exe, we want to use .reticulum-meshchat in the portable exe dir
// e.g if we launch "E:\Some\Path\MeshChat.exe" we want to use "E:\Some\Path\.reticulum-meshchat"
const portableExecutableDir = process.env.PORTABLE_EXECUTABLE_DIR;
if (process.platform === "win32" && portableExecutableDir != null) {
return path.join(portableExecutableDir, ".reticulum-meshchat");
}
// otherwise, we will fall back to putting the storage dir in the users home directory
// e.g: ~/.reticulum-meshchat
return path.join(app.getPath("home"), ".reticulum-meshchat");
}
function getDefaultReticulumConfigDir() {
// if we are running a windows portable exe, we want to use .reticulum in the portable exe dir
// e.g if we launch "E:\Some\Path\MeshChat.exe" we want to use "E:\Some\Path\.reticulum"
const portableExecutableDir = process.env.PORTABLE_EXECUTABLE_DIR;
if (process.platform === "win32" && portableExecutableDir != null) {
return path.join(portableExecutableDir, ".reticulum");
}
// otherwise, we will fall back to using the .reticulum folder in the users home directory
// e.g: ~/.reticulum
return path.join(app.getPath("home"), ".reticulum");
}
app.whenReady().then(async () => {
// get arguments passed to application, and remove the provided application path
const ignoredArguments = ["--no-sandbox", "--ozone-platform-hint=auto"];
const userProvidedArguments = process.argv.slice(1).filter((arg) => !ignoredArguments.includes(arg));
const shouldLaunchHeadless = userProvidedArguments.includes("--headless");
if (!shouldLaunchHeadless) {
// create browser window
mainWindow = new BrowserWindow({
width: 1500,
height: 800,
webPreferences: {
// used to inject logging over ipc
preload: path.join(__dirname, "preload.js"),
// Security: disable node integration in renderer
nodeIntegration: false,
// Security: enable context isolation (default in Electron 12+)
contextIsolation: true,
// Security: enable sandbox for additional protection
sandbox: true,
// Security: disable remote module (deprecated but explicit)
enableRemoteModule: false,
},
});
// open external links in default web browser instead of electron
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
var shouldShowInNewElectronWindow = false;
// we want to open call.html in a new electron window
// but all other target="_blank" links should open in the system web browser
// we don't want /rnode-flasher/index.html to open in electron, otherwise user can't select usb devices...
if (
(url.startsWith("http://localhost") || url.startsWith("https://localhost")) &&
url.includes("/call.html")
) {
shouldShowInNewElectronWindow = true;
}
// we want to open blob urls in a new electron window
else if (url.startsWith("blob:")) {
shouldShowInNewElectronWindow = true;
}
// open in new electron window
if (shouldShowInNewElectronWindow) {
return {
action: "allow",
};
}
// fallback to opening any other url in external browser
shell.openExternal(url);
return {
action: "deny",
};
});
// navigate to loading page
await mainWindow.loadFile(path.join(__dirname, "loading.html"));
// ask mac users for microphone access for audio calls to work
if (process.platform === "darwin") {
await systemPreferences.askForMediaAccess("microphone");
}
}
// find path to python/cxfreeze reticulum meshchat executable
// Note: setup.py creates ReticulumMeshChatX (with X), not ReticulumMeshChat
const exeName = process.platform === "win32" ? "ReticulumMeshChatX.exe" : "ReticulumMeshChatX";
// get app path (handles both development and packaged app)
const appPath = app.getAppPath();
// get resources path (where extraFiles are placed)
const resourcesPath = process.resourcesPath || path.join(appPath, "..", "..");
var exe = null;
// when packaged, extraFiles are placed at resources/app/electron/build/exe
// when packaged with asar, unpacked files are in app.asar.unpacked/ directory
// app.getAppPath() returns the path to app.asar, so unpacked is at the same level
const possiblePaths = [
// packaged app - extraFiles location (resources/app/electron/build/exe)
path.join(resourcesPath, "app", "electron", "build", "exe", exeName),
// packaged app with asar (unpacked files from asarUnpack)
path.join(appPath, "..", "app.asar.unpacked", "build", "exe", exeName),
// packaged app without asar (relative to app path)
path.join(appPath, "build", "exe", exeName),
// development mode (relative to electron directory)
path.join(__dirname, "build", "exe", exeName),
// development mode (relative to project root)
path.join(__dirname, "..", "build", "exe", exeName),
];
// find the first path that exists
for (const possibleExe of possiblePaths) {
if (fs.existsSync(possibleExe)) {
exe = possibleExe;
break;
}
}
// verify executable exists
if (!exe || !fs.existsSync(exe)) {
const errorMsg = `Could not find executable: ${exeName}\nChecked paths:\n${possiblePaths.join("\n")}\n\nApp path: ${appPath}\nResources path: ${resourcesPath}`;
log(errorMsg);
if (mainWindow) {
await dialog.showMessageBox(mainWindow, {
message: errorMsg,
});
}
app.quit();
return;
}
log(`Found executable at: ${exe}`);
try {
// arguments we always want to pass in
const requiredArguments = [
"--headless", // reticulum meshchat usually launches default web browser, we don't want this when using electron
"--port",
"9337", // FIXME: let system pick a random unused port?
// '--test-exception-message', 'Test Exception Message', // uncomment to test the crash dialog
];
// if user didn't provide reticulum config dir, we should provide it
if (!userProvidedArguments.includes("--reticulum-config-dir")) {
requiredArguments.push("--reticulum-config-dir", getDefaultReticulumConfigDir());
}
// if user didn't provide storage dir, we should provide it
if (!userProvidedArguments.includes("--storage-dir")) {
requiredArguments.push("--storage-dir", getDefaultStorageDir());
}
// spawn executable
exeChildProcess = await spawn(exe, [
...requiredArguments, // always provide required arguments
...userProvidedArguments, // also include any user provided arguments
]);
// log stdout
var stdoutLines = [];
exeChildProcess.stdout.setEncoding("utf8");
exeChildProcess.stdout.on("data", function (data) {
// log
log(data.toString());
// keep track of last 10 stdout lines
stdoutLines.push(data.toString());
if (stdoutLines.length > 10) {
stdoutLines.shift();
}
});
// log stderr
var stderrLines = [];
exeChildProcess.stderr.setEncoding("utf8");
exeChildProcess.stderr.on("data", function (data) {
// log
log(data.toString());
// keep track of last 10 stderr lines
stderrLines.push(data.toString());
if (stderrLines.length > 10) {
stderrLines.shift();
}
});
// log errors
exeChildProcess.on("error", function (error) {
log(error);
});
// quit electron app if exe dies
exeChildProcess.on("exit", async function (code) {
// if no exit code provided, we wanted exit to happen, so do nothing
if (code == null) {
return;
}
// tell user that Visual C++ redistributable needs to be installed on Windows
if (code === 3221225781 && process.platform === "win32") {
await dialog.showMessageBox(mainWindow, {
message: "Microsoft Visual C++ redistributable must be installed to run this application.",
});
app.quit();
return;
}
// show crash log
const stdout = stdoutLines.join("");
const stderr = stderrLines.join("");
await dialog.showMessageBox(mainWindow, {
message: [
"MeshChat Crashed!",
"",
`Exit Code: ${code}`,
"",
`----- stdout -----`,
"",
stdout,
`----- stderr -----`,
"",
stderr,
].join("\n"),
});
// quit after dismissing error dialog
app.quit();
});
} catch (e) {
log(e);
}
});
function quit() {
// kill python process
if (exeChildProcess) {
exeChildProcess.kill("SIGKILL");
}
// quit electron app
app.quit();
}
// quit electron if all windows are closed
app.on("window-all-closed", () => {
quit();
});
// make sure child process is killed if app is quiting
app.on("quit", () => {
quit();
});

View File

@@ -1,20 +1,188 @@
const { app, BrowserWindow, dialog, ipcMain, shell, systemPreferences } = require("electron");
const {
app,
BrowserWindow,
dialog,
ipcMain,
shell,
systemPreferences,
Tray,
Menu,
Notification,
powerSaveBlocker,
session,
} = require("electron");
const electronPrompt = require("electron-prompt");
const { spawn } = require("child_process");
const fs = require("fs");
const path = require("node:path");
const crypto = require("crypto");
// remember main window
var mainWindow = null;
// tray instance
var tray = null;
// power save blocker id
var activePowerSaveBlockerId = null;
// track if we are actually quiting
var isQuiting = false;
// remember child process for exe so we can kill it when app exits
var exeChildProcess = null;
// store integrity status
var integrityStatus = {
backend: { ok: true, issues: [] },
data: { ok: true, issues: [] },
};
// Check for hardware acceleration preference in storage dir
try {
const storageDir = getDefaultStorageDir();
const disableGpuFile = path.join(storageDir, "disable-gpu");
if (fs.existsSync(disableGpuFile)) {
app.disableHardwareAcceleration();
console.log("Hardware acceleration disabled via storage flag.");
}
} catch {
// ignore errors reading storage dir this early
}
// Handle hardware acceleration disabling via CLI
if (process.argv.includes("--disable-gpu") || process.argv.includes("--disable-software-rasterizer")) {
app.disableHardwareAcceleration();
}
// Protocol registration
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient("lxmf", process.execPath, [path.resolve(process.argv[1])]);
app.setAsDefaultProtocolClient("rns", process.execPath, [path.resolve(process.argv[1])]);
}
} else {
app.setAsDefaultProtocolClient("lxmf");
app.setAsDefaultProtocolClient("rns");
}
// Single instance lock
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
app.on("second-instance", (event, commandLine) => {
// Someone tried to run a second instance, we should focus our window.
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.show();
mainWindow.focus();
// Handle protocol links from second instance
const url = commandLine.pop();
if (url && (url.startsWith("lxmf://") || url.startsWith("rns://"))) {
mainWindow.webContents.send("open-protocol-link", url);
}
}
});
}
// Handle protocol links on macOS
app.on("open-url", (event, url) => {
event.preventDefault();
if (mainWindow) {
mainWindow.show();
mainWindow.webContents.send("open-protocol-link", url);
}
});
function verifyBackendIntegrity(exeDir) {
const manifestPath = path.join(__dirname, "backend-manifest.json");
if (!fs.existsSync(manifestPath)) {
log("Backend integrity manifest missing, skipping check.");
return { ok: true, issues: ["Manifest missing"] };
}
try {
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
const issues = [];
// The exeDir is build/exe when running or unpacked
// we only care about files in the manifest
for (const [relPath, expectedHash] of Object.entries(manifest)) {
const fullPath = path.join(exeDir, relPath);
if (!fs.existsSync(fullPath)) {
issues.push(`Missing: ${relPath}`);
continue;
}
const fileBuffer = fs.readFileSync(fullPath);
const actualHash = crypto.createHash("sha256").update(fileBuffer).digest("hex");
if (actualHash !== expectedHash) {
issues.push(`Modified: ${relPath}`);
}
}
return {
ok: issues.length === 0,
issues: issues,
};
} catch (error) {
log(`Backend integrity check failed: ${error.message}`);
return { ok: false, issues: [error.message] };
}
}
// allow fetching app version via ipc
ipcMain.handle("app-version", () => {
return app.getVersion();
});
// allow fetching hardware acceleration status via ipc
ipcMain.handle("is-hardware-acceleration-enabled", () => {
return app.isHardwareAccelerationEnabled();
});
// allow fetching integrity status
ipcMain.handle("get-integrity-status", () => {
return integrityStatus;
});
// Native Notification IPC
ipcMain.handle("show-notification", (event, { title, body, silent }) => {
const notification = new Notification({
title: title,
body: body,
silent: silent,
});
notification.show();
notification.on("click", () => {
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
});
});
// Power Management IPC
ipcMain.handle("set-power-save-blocker", (event, enabled) => {
if (enabled) {
if (activePowerSaveBlockerId === null) {
activePowerSaveBlockerId = powerSaveBlocker.start("prevent-app-suspension");
log("Power save blocker started.");
}
} else {
if (activePowerSaveBlockerId !== null) {
powerSaveBlocker.stop(activePowerSaveBlockerId);
activePowerSaveBlockerId = null;
log("Power save blocker stopped.");
}
}
return activePowerSaveBlockerId !== null;
});
// ignore ssl errors
app.commandLine.appendSwitch("ignore-certificate-errors");
@@ -63,6 +231,19 @@ ipcMain.handle("relaunch", () => {
app.exit();
});
ipcMain.handle("relaunch-emergency", () => {
app.relaunch({ args: process.argv.slice(1).concat(["--emergency"]) });
app.exit();
});
ipcMain.handle("shutdown", () => {
quit();
});
ipcMain.handle("get-memory-usage", async () => {
return process.getProcessMemoryInfo();
});
// allow showing a file path in os file manager
ipcMain.handle("showPathInFolder", (event, path) => {
shell.showItemInFolder(path);
@@ -112,7 +293,79 @@ function getDefaultReticulumConfigDir() {
return path.join(app.getPath("home"), ".reticulum");
}
function createTray() {
const iconPath = path.join(__dirname, "build", "icon.png");
const fallbackIconPath = path.join(__dirname, "assets", "images", "logo.png");
const trayIcon = fs.existsSync(iconPath) ? iconPath : fallbackIconPath;
tray = new Tray(trayIcon);
const contextMenu = Menu.buildFromTemplate([
{
label: "Show App",
click: function () {
if (mainWindow) {
mainWindow.show();
}
},
},
{
label: "Quit",
click: function () {
isQuiting = true;
quit();
},
},
]);
tray.setToolTip("Reticulum MeshChatX");
tray.setContextMenu(contextMenu);
tray.on("click", () => {
if (mainWindow) {
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
mainWindow.show();
}
}
});
}
app.whenReady().then(async () => {
// Security: Enforce CSP for all requests as a shell-level fallback
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
const responseHeaders = { ...details.responseHeaders };
// Define a robust fallback CSP that matches our backend's policy
const fallbackCsp = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob: https://*.tile.openstreetmap.org https://tile.openstreetmap.org",
"font-src 'self' data:",
"connect-src 'self' http://localhost:9337 https://localhost:9337 ws://localhost:* wss://localhost:* blob: https://*.tile.openstreetmap.org https://tile.openstreetmap.org https://nominatim.openstreetmap.org",
"media-src 'self' blob:",
"worker-src 'self' blob:",
"frame-src 'self'",
"object-src 'none'",
"base-uri 'self'",
].join("; ");
// If the response doesn't already have a CSP, apply our fallback
if (!responseHeaders["Content-Security-Policy"] && !responseHeaders["content-security-policy"]) {
responseHeaders["Content-Security-Policy"] = [fallbackCsp];
}
callback({ responseHeaders });
});
// Log Hardware Acceleration status (New in Electron 39)
const isHardwareAccelerationEnabled = app.isHardwareAccelerationEnabled();
log(`Hardware Acceleration Enabled: ${isHardwareAccelerationEnabled}`);
// Create system tray
createTray();
// get arguments passed to application, and remove the provided application path
const ignoredArguments = ["--no-sandbox", "--ozone-platform-hint=auto"];
const userProvidedArguments = process.argv.slice(1).filter((arg) => !ignoredArguments.includes(arg));
@@ -137,6 +390,15 @@ app.whenReady().then(async () => {
},
});
// minimize to tray behavior
mainWindow.on("close", (event) => {
if (!isQuiting) {
event.preventDefault();
mainWindow.hide();
return false;
}
});
// open external links in default web browser instead of electron
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
var shouldShowInNewElectronWindow = false;
@@ -160,6 +422,16 @@ app.whenReady().then(async () => {
if (shouldShowInNewElectronWindow) {
return {
action: "allow",
overrideBrowserWindowOptions: {
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
enableRemoteModule: false,
},
},
};
}
@@ -228,6 +500,13 @@ app.whenReady().then(async () => {
log(`Found executable at: ${exe}`);
// Verify backend integrity before spawning
const exeDir = path.dirname(exe);
integrityStatus.backend = verifyBackendIntegrity(exeDir);
if (!integrityStatus.backend.ok) {
log(`INTEGRITY WARNING: Backend tampering detected! Issues: ${integrityStatus.backend.issues.join(", ")}`);
}
try {
// arguments we always want to pass in
const requiredArguments = [
@@ -260,9 +539,9 @@ app.whenReady().then(async () => {
// log
log(data.toString());
// keep track of last 10 stdout lines
// keep track of last 100 stdout lines
stdoutLines.push(data.toString());
if (stdoutLines.length > 10) {
if (stdoutLines.length > 100) {
stdoutLines.shift();
}
});
@@ -274,9 +553,9 @@ app.whenReady().then(async () => {
// log
log(data.toString());
// keep track of last 10 stderr lines
// keep track of last 100 stderr lines
stderrLines.push(data.toString());
if (stderrLines.length > 10) {
if (stderrLines.length > 100) {
stderrLines.shift();
}
});
@@ -293,35 +572,34 @@ app.whenReady().then(async () => {
return;
}
// tell user that Visual C++ redistributable needs to be installed on Windows
if (code === 3221225781 && process.platform === "win32") {
await dialog.showMessageBox(mainWindow, {
message: "Microsoft Visual C++ redistributable must be installed to run this application.",
});
app.quit();
return;
}
// show crash log
const stdout = stdoutLines.join("");
const stderr = stderrLines.join("");
await dialog.showMessageBox(mainWindow, {
message: [
"MeshChat Crashed!",
"",
`Exit Code: ${code}`,
"",
`----- stdout -----`,
"",
stdout,
`----- stderr -----`,
"",
stderr,
].join("\n"),
});
// quit after dismissing error dialog
app.quit();
// Base64 encode for safe URL passing
const stdoutBase64 = Buffer.from(stdout).toString("base64");
const stderrBase64 = Buffer.from(stderr).toString("base64");
// Load crash page if main window exists
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.show(); // Ensure visible
mainWindow.focus();
await mainWindow.loadFile(path.join(__dirname, "crash.html"), {
query: {
code: code.toString(),
stdout: stdoutBase64,
stderr: stderrBase64,
},
});
} else {
// Fallback for cases where window is gone
await dialog.showMessageBox({
type: "error",
title: "MeshChatX Crashed",
message: `Backend exited with code: ${code}\n\nSTDOUT: ${stdout.slice(-500)}\n\nSTDERR: ${stderr.slice(-500)}`,
});
app.quit();
}
});
} catch (e) {
log(e);

View File

@@ -9,6 +9,21 @@ contextBridge.exposeInMainWorld("electron", {
return await ipcRenderer.invoke("app-version");
},
// allow fetching electron version
electronVersion: function () {
return process.versions.electron;
},
// allow fetching chrome version
chromeVersion: function () {
return process.versions.chrome;
},
// allow fetching node version
nodeVersion: function () {
return process.versions.node;
},
// show an alert dialog in electron browser window, this fixes a bug where alert breaks input fields on windows
alert: async function (message) {
return await ipcRenderer.invoke("alert", message);
@@ -29,8 +44,43 @@ contextBridge.exposeInMainWorld("electron", {
return await ipcRenderer.invoke("relaunch");
},
// allow relaunching app in emergency mode
relaunchEmergency: async function () {
return await ipcRenderer.invoke("relaunch-emergency");
},
// allow shutting down app in electron browser window
shutdown: async function () {
return await ipcRenderer.invoke("shutdown");
},
// allow getting memory usage in electron browser window
getMemoryUsage: async function () {
return await ipcRenderer.invoke("get-memory-usage");
},
// allow showing a file path in os file manager
showPathInFolder: async function (path) {
return await ipcRenderer.invoke("showPathInFolder", path);
},
// allow checking hardware acceleration status
isHardwareAccelerationEnabled: async function () {
return await ipcRenderer.invoke("is-hardware-acceleration-enabled");
},
// allow checking integrity status
getIntegrityStatus: async function () {
return await ipcRenderer.invoke("get-integrity-status");
},
// allow showing a native notification
showNotification: function (title, body, silent = false) {
ipcRenderer.invoke("show-notification", { title, body, silent });
},
// allow controlling power save blocker
setPowerSaveBlocker: async function (enabled) {
return await ipcRenderer.invoke("set-power-save-blocker", enabled);
},
// listen for protocol links
onProtocolLink: function (callback) {
ipcRenderer.on("open-protocol-link", (event, url) => callback(url));
},
});