diff --git a/CHANGELOG.md b/CHANGELOG.md index 5788e1b7..bca78d16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ All notable changes to this project will be documented in this file. - Relay Chat room keys so hosts can require a key to join a room - Desktop privacy: Windows screen security to omit MeshChatX from screenshots, recording, and Recall - Android privacy options to block screenshots and clear the clipboard when backgrounded +- Messages: **Copy image to clipboard** on the message and full-screen image context menus - Remote management allow-list for identities that may query this instance with rnstatus/rnpath - Post-install prompts for existing users after upgrades - Coolify-oriented Docker Compose with resource limits for deployments @@ -56,6 +57,7 @@ All notable changes to this project will be documented in this file. ### Fixed +- Desktop AppImage: closed stdout no longer raises a main-process **write EPIPE** dialog when MeshChatX is backgrounded. Logging uses broken-pipe guards - Android: lxmfy packaging, flock soft-lock, splash/logo clipping, Landlock skipped on Android - Android RNode BLE/USB via Chaquopy - Startup check and disable unsupported interfaces diff --git a/electron/main.js b/electron/main.js index a1215829..3b1b24f0 100644 --- a/electron/main.js +++ b/electron/main.js @@ -42,6 +42,9 @@ const { applyContentProtection, applyContentProtectionToWindows, } = require("./desktopPrivacySettings"); +const { installBrokenPipeGuards, safeConsoleLog } = require("./safeConsole"); + +installBrokenPipeGuards(process); // remember main window var mainWindow = null; @@ -84,7 +87,7 @@ try { const disableGpuFile = path.join(storageDir, "disable-gpu"); if (fs.existsSync(disableGpuFile)) { app.disableHardwareAcceleration(); - console.log("Hardware acceleration disabled via storage flag."); + safeConsoleLog("Hardware acceleration disabled via storage flag."); } } catch { // ignore errors reading storage dir this early @@ -102,7 +105,7 @@ if (process.platform === "linux") { // Detect if running in Flatpak sandbox const isRunningInFlatpak = !!process.env.FLATPAK_ID; if (isRunningInFlatpak) { - console.log(`Running in Flatpak sandbox: ${process.env.FLATPAK_ID}`); + safeConsoleLog(`Running in Flatpak sandbox: ${process.env.FLATPAK_ID}`); } // Protocol registration @@ -629,6 +632,26 @@ function attachDefaultContextMenu(browserWindow) { }); } + 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; } @@ -639,8 +662,8 @@ function attachDefaultContextMenu(browserWindow) { } function log(message) { - // log to stdout of this process - console.log(message); + // log to stdout of this process (AppImage may close the pipe later) + safeConsoleLog(message); // make sure main window exists if (!mainWindow) { diff --git a/electron/safeConsole.js b/electron/safeConsole.js new file mode 100644 index 00000000..68c6512e --- /dev/null +++ b/electron/safeConsole.js @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: 0BSD + +/** + * Broken-pipe (EPIPE) guards for Electron AppImage / launcher sessions where + * stdout or stderr is closed while the main process still logs. + */ + +function isBrokenPipeError(err) { + if (!err || typeof err !== "object") { + return false; + } + if (err.code === "EPIPE" || err.errno === "EPIPE") { + return true; + } + const message = typeof err.message === "string" ? err.message : ""; + return /\bEPIPE\b/i.test(message); +} + +/** + * Attach no-op error listeners so closed stdout/stderr do not become + * uncaughtException dialogs when console.log writes after the pipe closes. + * @param {{ stdout?: NodeJS.WritableStream, stderr?: NodeJS.WritableStream } | null | undefined} [proc] + */ +function installBrokenPipeGuards(proc = process) { + const attach = (stream) => { + if (!stream || typeof stream.on !== "function") { + return; + } + if (stream.__meshchatxBrokenPipeGuarded) { + return; + } + stream.__meshchatxBrokenPipeGuarded = true; + stream.on("error", (err) => { + if (isBrokenPipeError(err)) { + return; + } + }); + }; + attach(proc?.stdout); + attach(proc?.stderr); +} + +/** + * console.log that never throws on a closed stdout pipe. + * @param {...unknown} args + */ +function safeConsoleLog(...args) { + try { + console.log(...args); + } catch (err) { + if (!isBrokenPipeError(err)) { + throw err; + } + } +} + +module.exports = { + isBrokenPipeError, + installBrokenPipeGuards, + safeConsoleLog, +}; diff --git a/meshchatx.rsm b/meshchatx.rsm index 495bdf4a..dbefb9ed 100644 Binary files a/meshchatx.rsm and b/meshchatx.rsm differ diff --git a/meshchatx/src/frontend/components/messages/ConversationViewer.vue b/meshchatx/src/frontend/components/messages/ConversationViewer.vue index 715053c8..adead7e3 100644 --- a/meshchatx/src/frontend/components/messages/ConversationViewer.vue +++ b/meshchatx/src/frontend/components/messages/ConversationViewer.vue @@ -968,6 +968,13 @@ {{ $t("messages.save_image_to_device") }} + + + {{ $t("messages.copy_image_to_clipboard") }} + {{ $t("messages.save_image_to_device") }} + + + {{ $t("messages.copy_image_to_clipboard") }} + @@ -1646,7 +1657,7 @@