const { app, BrowserWindow, dialog, ipcMain, shell, systemPreferences, Tray, Menu, Notification, powerSaveBlocker, powerMonitor, session, clipboard, } = require("electron"); const electronPrompt = require("electron-prompt"); const fs = require("fs"); const path = require("node:path"); const { createBackendProcessManager } = require("./backendProcess"); const { readPackagedAppVersion } = require("./appVersion"); const { getCrashRecoveryInfo } = require("./offlineRecovery"); const { getUserProvidedArguments, resolvePortableStorageRoots, formatRenderProcessGoneDetails, isLocalBackendUrl, shouldOpenInElectronWindow, shouldAllowInWindowNavigation, isTrustedIpcEvent, } = require("./mainHelpers"); const { isAllowedShellPath } = require("./shellPathGuard"); const { normalizeExternalUrlForOpen } = require("./safeExternalUrl"); const { attachHardwareDevicePermissionHandlers } = require("./hardwareDevicePermissions"); const { loadCloseSettings, saveCloseSettings, resolveCloseAction, rememberedCloseSettings, createCloseRequestGuard, } = require("./closeBehavior"); const { loadDesktopPrivacySettings, saveDesktopPrivacySettings, isScreenSecurityPlatformSupported, applyContentProtection, applyContentProtectionToWindows, } = require("./desktopPrivacySettings"); const { getLogsDir } = require("./backendCrashReport"); const { installBrokenPipeGuards, createMainProcessLogger } = require("./safeConsole"); function resolvePreloadScriptPath() { const bundled = path.join(__dirname, "preload.bundle.js"); if (fs.existsSync(bundled)) { return bundled; } return path.join(__dirname, "preload.js"); } installBrokenPipeGuards(process); const mainProcessLogger = createMainProcessLogger({ getLogsDir: () => getLogsDir(getDefaultStorageDir()), }); // remember main window var mainWindow = null; var closeSettings = null; var desktopPrivacySettings = null; var closeRequestGuard = createCloseRequestGuard(); function getDialogParentWindow() { const focused = BrowserWindow.getFocusedWindow(); if (focused && !focused.isDestroyed()) { return focused; } if (mainWindow && !mainWindow.isDestroyed()) { return mainWindow; } return null; } // tray instance var tray = null; // power save blocker id var activePowerSaveBlockerId = null; // track if we are actually quiting var isQuiting = false; // backend child process (managed by backendProcess.js) var backendManager = 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(); mainProcessLogger.write("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(); } if (process.platform === "linux") { app.setName("reticulum-meshchatx"); } // Detect if running in Flatpak sandbox const isRunningInFlatpak = !!process.env.FLATPAK_ID; if (isRunningInFlatpak) { mainProcessLogger.write(`Running in Flatpak sandbox: ${process.env.FLATPAK_ID}`); } // 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 trustedIpcHandle(channel, listener) { ipcMain.handle(channel, async (event, ...args) => { if (!isTrustedIpcEvent(event)) { throw new Error("MeshChatX IPC blocked for this origin"); } return listener(event, ...args); }); } // allow fetching app version via ipc trustedIpcHandle("app-version", () => { return readPackagedAppVersion(app.getVersion()); }); // allow fetching hardware acceleration status via ipc trustedIpcHandle("is-hardware-acceleration-enabled", () => { return app.isHardwareAccelerationEnabled(); }); // allow fetching integrity status trustedIpcHandle("get-integrity-status", () => { return integrityStatus; }); // Native Notification IPC const { trackMessageNotification, untrackMessageNotification, closeMessageNotificationsFor, closeAllMessageNotifications, } = require("./messageNotifications.js"); trustedIpcHandle("show-notification", (event, { title, body, silent, destinationHash }) => { const notification = new Notification({ title: title, body: body, silent: silent, }); trackMessageNotification(destinationHash, notification); notification.on("close", () => { untrackMessageNotification(destinationHash, notification); }); notification.show(); notification.on("click", () => { if (mainWindow) { mainWindow.show(); mainWindow.focus(); } }); }); trustedIpcHandle("close-message-notifications", (_event, destinationHash) => { if (destinationHash) { return closeMessageNotificationsFor(destinationHash); } return closeAllMessageNotifications(); }); // Power Management IPC trustedIpcHandle("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"); trustedIpcHandle("backend-http-only", () => { return getUserProvidedArguments(process.argv).includes("--no-https"); }); trustedIpcHandle("backend-runtime-state", () => { return getBackendManager().getRuntimeState(); }); trustedIpcHandle("backend-startup-diagnostics", () => { return getBackendManager().getStartupDiagnostics(); }); trustedIpcHandle("mark-backend-healthy", () => { getBackendManager().markBackendHealthy(); return { ok: true }; }); trustedIpcHandle("restart-backend", async () => { return await getBackendManager().restartBackend(integrityStatus); }); trustedIpcHandle("open-backend-crash-report", async () => { const lastCrash = getBackendManager().getLastCrash(); if (!lastCrash) { return { ok: false, error: "No backend crash report is available." }; } await loadBackendCrashPage(lastCrash); return { ok: true }; }); trustedIpcHandle("crash-recovery-info", () => { const manager = getBackendManager(); const lastCrash = manager.getLastCrash() || {}; const logs = manager.getJoinedLogs(); return getCrashRecoveryInfo({ storageDir: getDefaultStorageDir(), reticulumConfigDir: getDefaultReticulumConfigDir(), platform: process.platform, portableExecutableDir: process.env.PORTABLE_EXECUTABLE_DIR || null, stderr: lastCrash.stderr || logs.stderr || "", stdout: lastCrash.stdout || logs.stdout || "", exitCode: lastCrash.code != null ? lastCrash.code : null, }); }); trustedIpcHandle("restore-database-backup", async (_event, backupPath) => { if (!backupPath || typeof backupPath !== "string") { return { ok: false, error: "No backup path provided." }; } const ctx = { app, getDefaultStorageDir, getDefaultReticulumConfigDir, getUserProvidedArguments, }; const { isAllowedShellPath } = require("./shellPathGuard"); if (!isAllowedShellPath(backupPath, ctx)) { return { ok: false, error: "Backup path is not allowed." }; } if (!fs.existsSync(backupPath)) { return { ok: false, error: "Backup file was not found." }; } return await getBackendManager().runMaintenanceTask(["--restore-db", backupPath]); }); trustedIpcHandle("pick-database-backup", async () => { const win = getDialogParentWindow(); if (!win) { return null; } const { canceled, filePaths } = await dialog.showOpenDialog(win, { properties: ["openFile"], filters: [{ name: "MeshChatX backup", extensions: ["zip"] }], }); if (canceled || !filePaths || filePaths.length === 0) { return null; } return filePaths[0]; }); // add support for showing an alert window via ipc trustedIpcHandle("alert", async (event, message) => { return await dialog.showMessageBox(mainWindow, { message: message, }); }); // add support for showing a confirm window via ipc trustedIpcHandle("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 trustedIpcHandle("prompt", async (event, message, defaultValue = "") => { return await electronPrompt({ title: message, label: "", value: defaultValue == null ? "" : String(defaultValue), type: "input", inputAttrs: { type: "text", }, }); }); // allow relaunching app via ipc trustedIpcHandle("relaunch", () => { const relaunchOptions = {}; if (!process.defaultApp && process.platform === "linux" && process.env.APPIMAGE) { relaunchOptions.execPath = process.env.APPIMAGE; } app.relaunch(relaunchOptions); isQuiting = true; quit(); }); trustedIpcHandle("relaunch-emergency", () => { const relaunchOptions = { args: process.argv.slice(1).concat(["--emergency"]), }; if (!process.defaultApp && process.platform === "linux" && process.env.APPIMAGE) { relaunchOptions.execPath = process.env.APPIMAGE; } app.relaunch(relaunchOptions); isQuiting = true; quit(); }); trustedIpcHandle("relaunch-auto-recover", () => { const relaunchOptions = { args: process.argv.slice(1).concat(["--auto-recover"]), }; if (!process.defaultApp && process.platform === "linux" && process.env.APPIMAGE) { relaunchOptions.execPath = process.env.APPIMAGE; } app.relaunch(relaunchOptions); isQuiting = true; quit(); }); trustedIpcHandle("shutdown", () => { isQuiting = true; quit(); }); trustedIpcHandle("get-close-settings", () => { return getCloseSettings(); }); trustedIpcHandle("set-close-settings", (_event, partial) => { return updateCloseSettings(partial || {}); }); trustedIpcHandle("get-screen-security-settings", () => { return getScreenSecuritySettingsPayload(); }); trustedIpcHandle("set-screen-security-enabled", (_event, enabled) => { return updateScreenSecurityEnabled(enabled === true); }); trustedIpcHandle("get-memory-usage", async () => { return process.getProcessMemoryInfo(); }); trustedIpcHandle("get-battery-status", async () => { let onBattery = null; try { if (typeof powerMonitor?.isOnBatteryPower === "function") { onBattery = Boolean(powerMonitor.isOnBatteryPower()); } } catch { onBattery = null; } let level = null; // Linux sysfs: common laptop BAT0/BAT1 capacity files. if (process.platform === "linux") { try { const powerSupplyDir = "/sys/class/power_supply"; if (fs.existsSync(powerSupplyDir)) { const entries = fs.readdirSync(powerSupplyDir); for (const name of entries) { if (!/^BAT\d+$/i.test(name) && name.toUpperCase() !== "BATTERY") { continue; } const capacityPath = path.join(powerSupplyDir, name, "capacity"); if (!fs.existsSync(capacityPath)) { continue; } const raw = fs.readFileSync(capacityPath, "utf8").trim(); const parsed = Number.parseInt(raw, 10); if (Number.isFinite(parsed) && parsed >= 0 && parsed <= 100) { level = parsed; break; } } } } catch { level = null; } } if (level == null && onBattery == null) { return null; } return { level, charging: onBattery == null ? null : !onBattery, on_battery: onBattery, source: "electron", }; }); // allow showing a file path in os file manager trustedIpcHandle("showPathInFolder", (event, targetPath) => { const ctx = { app, getDefaultStorageDir, getDefaultReticulumConfigDir, getUserProvidedArguments, }; if (!isAllowedShellPath(targetPath, ctx)) { console.warn("showPathInFolder denied (path outside allowed directories)"); return; } shell.showItemInFolder(targetPath); }); trustedIpcHandle("open-path", (event, targetPath) => { const ctx = { app, getDefaultStorageDir, getDefaultReticulumConfigDir, getUserProvidedArguments, }; if (!isAllowedShellPath(targetPath, ctx)) { console.warn("open-path denied (path outside allowed directories)"); return "Path is not allowed"; } return shell.openPath(targetPath); }); trustedIpcHandle("pick-file", async () => { const win = getDialogParentWindow(); if (!win) { return null; } const { canceled, filePaths } = await dialog.showOpenDialog(win, { properties: ["openFile"], }); if (canceled || !filePaths || filePaths.length === 0) { return null; } return filePaths[0]; }); trustedIpcHandle("pick-directory", async () => { const win = getDialogParentWindow(); if (!win) { return null; } const { canceled, filePaths } = await dialog.showOpenDialog(win, { properties: ["openDirectory"], }); if (canceled || !filePaths || filePaths.length === 0) { return null; } return filePaths[0]; }); function getChildBrowserWindowOptions() { return { autoHideMenuBar: true, webPreferences: { preload: resolvePreloadScriptPath(), nodeIntegration: false, contextIsolation: true, sandbox: true, enableRemoteModule: false, }, }; } function handleWindowOpenRequest(url) { if (shouldOpenInElectronWindow(url)) { return { action: "allow", overrideBrowserWindowOptions: getChildBrowserWindowOptions(), }; } const safe = normalizeExternalUrlForOpen(url); if (safe) { shell.openExternal(safe); } return { action: "deny", }; } function attachWindowOpenHandler(webContents) { webContents.setWindowOpenHandler(({ url }) => handleWindowOpenRequest(url)); } function denyUntrustedInWindowNavigation(event, url, openExternalIfHttp) { if (shouldAllowInWindowNavigation(url)) { return; } event.preventDefault(); if (!openExternalIfHttp) { return; } const safe = normalizeExternalUrlForOpen(url); if (safe) { shell.openExternal(safe); } } function attachInWindowNavigationGuard(webContents) { webContents.on("will-navigate", (event, url) => { denyUntrustedInWindowNavigation(event, url, true); }); webContents.on("will-redirect", (event, url) => { denyUntrustedInWindowNavigation(event, url, true); }); webContents.on("will-frame-navigate", (event, url, isMainFrame) => { denyUntrustedInWindowNavigation(event, url, isMainFrame === true); }); webContents.on("will-attach-webview", (event) => { event.preventDefault(); }); } app.on("web-contents-created", (_event, contents) => { attachWindowOpenHandler(contents); attachInWindowNavigationGuard(contents); }); function attachDevToolsF12Shortcut(browserWindow) { browserWindow.webContents.on("before-input-event", (event, input) => { if (input.type !== "keyDown" || input.key !== "F12") { return; } if (browserWindow.isDestroyed()) { return; } browserWindow.webContents.toggleDevTools(); event.preventDefault(); }); } function attachDefaultContextMenu(browserWindow) { const webContents = browserWindow.webContents; webContents.on("context-menu", (event, params) => { const template = []; if (params.isEditable) { template.push( { role: "undo" }, { role: "redo" }, { type: "separator" }, { role: "cut" }, { role: "copy" }, { role: "paste" }, { role: "pasteAndMatchStyle" }, { type: "separator" }, { role: "selectAll" } ); } else if (params.selectionText) { template.push({ role: "copy" }); } if (params.misspelledWord) { const suggestions = params.dictionarySuggestions || []; if (suggestions.length > 0) { if (template.length > 0) { template.push({ type: "separator" }); } for (const suggestion of suggestions) { template.push({ label: suggestion, click: () => { webContents.replaceMisspelling(suggestion); }, }); } } if (template.length > 0) { template.push({ type: "separator" }); } template.push({ label: "Add to dictionary", click: () => { void webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord); }, }); } if (params.linkURL) { if (template.length > 0) { template.push({ type: "separator" }); } template.push({ label: "Open link", click: () => { const safe = normalizeExternalUrlForOpen(params.linkURL); if (safe) { shell.openExternal(safe); } }, }); template.push({ label: "Copy link", click: () => { clipboard.writeText(params.linkURL); }, }); } if (params.mediaType === "image" || params.hasImageContents) { if (template.length > 0) { template.push({ type: "separator" }); } template.push({ label: "Copy image", click: () => { webContents.copyImageAt(params.x, params.y); }, }); if (params.srcURL) { template.push({ label: "Copy image address", click: () => { clipboard.writeText(params.srcURL); }, }); } } if (template.length === 0) { return; } const menu = Menu.buildFromTemplate(template); menu.popup({ window: browserWindow }); }); } function log(message) { // Durable file under storage/logs/meshchatx.log. Stdout only when a TTY // is attached (desktop AppImage launchers usually close the pipe). mainProcessLogger.write(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); } // Resolves storage and Reticulum config roots the same way on every platform: // explicit --storage-dir/--reticulum-config-dir (flag or env) first, then // --data-dir/MESHCHAT_DATA_DIR (portable root), then a Windows portable exe // directory, then the user home directory. See mainHelpers.resolvePortableStorageRoots. function getPortableStorageRoots() { return resolvePortableStorageRoots({ argv: process.argv, env: process.env, homeDir: app.getPath("home"), isWindows: process.platform === "win32", portableExecutableDir: process.env.PORTABLE_EXECUTABLE_DIR, }); } function getDefaultStorageDir() { return getPortableStorageRoots().storageDir; } function getDefaultReticulumConfigDir() { return getPortableStorageRoots().reticulumConfigDir; } function getAppIconPath() { const iconPath = path.join(__dirname, "build", "icon.png"); const fallbackIconPath = path.join(__dirname, "assets", "images", "logo.png"); return fs.existsSync(iconPath) ? iconPath : fallbackIconPath; } function getMainWindowPageKind() { if (!mainWindow || mainWindow.isDestroyed()) { return "none"; } const url = mainWindow.webContents.getURL(); if (url.includes("loading.html")) { return "loading"; } if (url.includes("crash.html")) { return "crash"; } if (isLocalBackendUrl(url)) { return "app"; } return "other"; } async function loadBackendCrashPage(crash) { const stdoutBase64 = Buffer.from((crash && crash.stdout) || "").toString("base64"); const stderrBase64 = Buffer.from((crash && crash.stderr) || "").toString("base64"); const code = crash && crash.code != null ? String(crash.code) : ""; const paths = getBackendManager().getStartupDiagnostics().paths; if (!mainWindow || mainWindow.isDestroyed()) { await dialog.showMessageBox({ type: "error", title: "MeshChatX Crashed", message: `Backend exited with code: ${code}`, }); app.quit(); return; } mainWindow.show(); mainWindow.focus(); await mainWindow.loadFile(path.join(__dirname, "crash.html"), { query: { code: code, stdout: stdoutBase64, stderr: stderrBase64, logPath: paths?.backendLogPath || "", crashReportPath: paths?.crashReportPath || "", }, }); } function getBackendManager() { if (!backendManager) { backendManager = createBackendProcessManager({ log, getDefaultStorageDir, getDefaultReticulumConfigDir, getMainWindowPageKind, isQuiting: () => quitInitiated, notifyRenderer: (channel, payload) => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send(channel, payload); } }, showCrashPage: loadBackendCrashPage, }); } return backendManager; } function getCloseSettings() { if (!closeSettings) { closeSettings = loadCloseSettings(getDefaultStorageDir()); } return closeSettings; } function updateCloseSettings(partial) { closeSettings = saveCloseSettings(getDefaultStorageDir(), partial); syncTrayWithSettings(); return closeSettings; } function getDesktopPrivacySettings() { if (!desktopPrivacySettings) { desktopPrivacySettings = loadDesktopPrivacySettings(getDefaultStorageDir()); } return desktopPrivacySettings; } function getScreenSecuritySettingsPayload() { const settings = getDesktopPrivacySettings(); const platform = process.platform; return { platform, available: isScreenSecurityPlatformSupported(platform), windowsDrm: platform === "win32", enabled: settings.screenSecurityEnabled === true, }; } function applyScreenSecurityToWindow(browserWindow, enabled) { if (!applyContentProtection(browserWindow, enabled === true)) { return; } } function applyScreenSecurityToAllWindows(enabled) { applyContentProtectionToWindows(BrowserWindow.getAllWindows(), enabled === true); } function updateScreenSecurityEnabled(enabled) { desktopPrivacySettings = saveDesktopPrivacySettings(getDefaultStorageDir(), { screenSecurityEnabled: enabled === true, }); applyScreenSecurityToAllWindows(desktopPrivacySettings.screenSecurityEnabled); return getScreenSecuritySettingsPayload(); } function destroyTray() { if (tray && !tray.isDestroyed()) { tray.destroy(); } tray = null; } function createTray() { if (tray && !tray.isDestroyed()) { return; } tray = new Tray(getAppIconPath()); 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(); } } }); } function syncTrayWithSettings() { const settings = getCloseSettings(); if (settings.trayEnabled) { createTray(); } else { destroyTray(); } } async function promptCloseAction() { const settings = getCloseSettings(); const backgroundLabel = settings.trayEnabled ? "Keep running in background" : "Minimize to taskbar"; const backgroundDetail = settings.trayEnabled ? "Hide the window and keep MeshChatX in the system tray." : "Minimize MeshChatX to the taskbar."; const result = await dialog.showMessageBox(getDialogParentWindow() || undefined, { type: "question", title: "Close MeshChatX?", message: "Close MeshChatX?", detail: `Choose whether to quit the application or keep it running.\n\n${backgroundDetail}`, buttons: ["Cancel", "Quit application", backgroundLabel], defaultId: 2, cancelId: 0, checkboxLabel: "Remember my choice", checkboxChecked: false, }); if (result.response === 0) { return null; } const action = result.response === 1 ? "quit" : settings.trayEnabled ? "background" : "minimize"; const remembered = rememberedCloseSettings(action, result.checkboxChecked); if (remembered) { updateCloseSettings(remembered); } return action; } async function handleWindowCloseRequest(event) { if (isQuiting) { return; } event.preventDefault(); if (!closeRequestGuard.tryEnter()) { return; } try { const settings = getCloseSettings(); let action = resolveCloseAction(settings); if (action === "ask") { action = await promptCloseAction(); if (!action) { return; } } if (action === "quit") { isQuiting = true; quit(); return; } if (!mainWindow || mainWindow.isDestroyed()) { return; } if (action === "minimize") { mainWindow.minimize(); return; } mainWindow.hide(); } finally { closeRequestGuard.leave(); } } app.whenReady().then(async () => { app.on("browser-window-created", (event, browserWindow) => { attachDefaultContextMenu(browserWindow); attachDevToolsF12Shortcut(browserWindow); const privacy = getDesktopPrivacySettings(); applyScreenSecurityToWindow(browserWindow, privacy.screenSecurityEnabled === true); }); attachHardwareDevicePermissionHandlers(session.defaultSession, { dialog, getParentWindow: getDialogParentWindow, }); // 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' 'wasm-unsafe-eval' blob:", "style-src 'self' 'unsafe-inline'", "img-src 'self' data: blob: https://*.tile.openstreetmap.org https://tile.openstreetmap.org https://*.cartocdn.com https://tiles.openfreemap.org https://*.openfreemap.org", "font-src 'self' data: https://tiles.openfreemap.org https://*.openfreemap.org", "connect-src 'self' http://127.0.0.1:9337 https://127.0.0.1:9337 http://localhost:9337 https://localhost:9337 ws://127.0.0.1:* wss://127.0.0.1:* ws://localhost:* wss://localhost:* blob: https://*.tile.openstreetmap.org https://tile.openstreetmap.org https://nominatim.openstreetmap.org https://github.com https://*.cartocdn.com https://tiles.openfreemap.org https://*.openfreemap.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 }); }); // Keep UI downloads under Downloads/MeshChatX so AppContainer ACLs and // reveal-in-folder stay on an app-owned exchange dir, not all of Downloads. try { const exchangeDownloads = path.join(app.getPath("downloads"), "MeshChatX"); fs.mkdirSync(exchangeDownloads, { recursive: true }); session.defaultSession.setDownloadPath(exchangeDownloads); log(`Download path set to ${exchangeDownloads}`); } catch (error) { log(`Failed to set MeshChatX download path: ${error && error.message ? error.message : error}`); } // Log Hardware Acceleration status (New in Electron 39) const isHardwareAccelerationEnabled = app.isHardwareAccelerationEnabled(); log(`Hardware Acceleration Enabled: ${isHardwareAccelerationEnabled}`); // Create system tray when enabled in desktop close settings syncTrayWithSettings(); // get arguments passed to application, and remove the provided application path const userProvidedArguments = getUserProvidedArguments(process.argv); const shouldLaunchHeadless = userProvidedArguments.includes("--headless"); if (!shouldLaunchHeadless) { const appIconPath = getAppIconPath(); // create browser window mainWindow = new BrowserWindow({ width: 1500, height: 800, icon: appIconPath, autoHideMenuBar: true, webPreferences: { // used to inject logging over ipc preload: resolvePreloadScriptPath(), // 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, }, }); mainWindow.webContents.on("render-process-gone", (_event, details) => { log(`Renderer process crashed: ${formatRenderProcessGoneDetails(details)}`); }); mainWindow.webContents.on("unresponsive", () => { log("Renderer process became unresponsive."); }); mainWindow.webContents.on( "did-fail-load", async (_event, errorCode, errorDescription, validatedURL, isMainFrame) => { if (!isMainFrame || !isLocalBackendUrl(validatedURL)) { return; } log(`Failed to load backend URL (${errorCode}): ${errorDescription} - ${validatedURL}`); if (!mainWindow || mainWindow.isDestroyed()) { return; } const currentUrl = mainWindow.webContents.getURL(); if (currentUrl.includes("loading.html")) { return; } try { await mainWindow.loadFile(path.join(__dirname, "loading.html"), { query: { startup_error: "backend_unreachable" }, }); } catch (error) { log(`Failed to restore loading screen after backend load failure: ${error.message}`); } } ); // quit / minimize / hide-to-tray based on remembered close settings mainWindow.on("close", (event) => { void handleWindowCloseRequest(event); }); // 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 executable (setup.py builds ReticulumMeshChatX) 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; const platformFolder = process.platform === "win32" || process.platform === "win" ? "win32" : process.platform === "darwin" ? "darwin" : "linux"; const archSegment = (() => { if (process.arch === "arm64") return "arm64"; if (process.arch === "ia32") return "ia32"; if (process.arch === "arm") return "armv7l"; return "x64"; })(); const packagedExtraResourceDir = path.join(resourcesPath, `${platformFolder}-${archSegment}`, exeName); // when packaged, extraResources are placed at resources/backend // when packaged with extraFiles, they were 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 - extraResources location (resources/backend) path.join(resourcesPath, "backend", exeName), // @electron/packager extraResource: copies build/exe/- to resources/-/ packagedExtraResourceDir, // legacy electron-forge extraResource location (resources/exe) path.join(resourcesPath, "exe", exeName), // legacy 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}`); const manager = getBackendManager(); manager.setUserProvidedArguments(userProvidedArguments); try { await manager.spawnBackend(exe, integrityStatus); } catch (e) { log(e); } }); app.on("render-process-gone", (_event, webContents, details) => { const wcId = webContents ? webContents.id : "unknown"; log(`render-process-gone for webContents ${wcId}: ${formatRenderProcessGoneDetails(details)}`); }); // Track if quit has been initiated to prevent recursion let quitInitiated = false; let quitTimeoutId = null; function quit() { if (quitInitiated) { return; } quitInitiated = true; const exeChildProcess = getBackendManager().getChildProcess(); if (!exeChildProcess) { app.quit(); return; } if (exeChildProcess.exitCode !== null || exeChildProcess.signalCode !== null) { app.quit(); return; } try { getBackendManager().killChild("SIGTERM"); } catch (e) { log(e); try { getBackendManager().killChild("SIGKILL"); } catch (e2) { log(e2); } app.quit(); return; } const timeoutMs = 5000; quitTimeoutId = setTimeout(() => { try { const proc = getBackendManager().getChildProcess(); if (proc && proc.exitCode === null && proc.signalCode === null) { getBackendManager().killChild("SIGKILL"); } } catch (e) { log(e); } app.quit(); }, timeoutMs); exeChildProcess.once("exit", () => { if (quitTimeoutId) { clearTimeout(quitTimeoutId); quitTimeoutId = null; } app.quit(); }); } // Handle before-quit for additional cleanup app.on("before-quit", () => { if (!isQuiting) { isQuiting = true; } // Ensure tray is destroyed to prevent it from keeping the app alive destroyTray(); }); // quit electron if all windows are closed app.on("window-all-closed", () => { quit(); });