mirror of
https://github.com/Quad4-Software/MeshChatX.git
synced 2026-08-18 09:49:09 -04:00
188 lines
5.2 KiB
JavaScript
188 lines
5.2 KiB
JavaScript
// SPDX-License-Identifier: 0BSD
|
|
|
|
/**
|
|
* Main-process log policy for Electron.
|
|
*
|
|
* Desktop launchers (AppImage, .desktop) often start MeshChatX with stdout
|
|
* attached to a pipe that later closes. Writing console.log then raises EPIPE.
|
|
* Prefer a durable file under the storage logs dir, and only mirror to stdout
|
|
* when it is still a TTY (or when forced via env).
|
|
*/
|
|
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
const ELECTRON_LOG_PREFIX = "[electron] ";
|
|
|
|
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 a write races the pipe close.
|
|
* @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);
|
|
}
|
|
|
|
/**
|
|
* True when stdout is a live terminal, or MESHCHAT_FORCE_STDOUT_LOG=1.
|
|
* Packaged AppImage / desktop launches usually have isTTY false.
|
|
* @param {{ stdout?: { isTTY?: boolean } } | null | undefined} [proc]
|
|
* @param {NodeJS.ProcessEnv} [env]
|
|
*/
|
|
function shouldMirrorStdout(proc = process, env = process.env) {
|
|
if (env?.MESHCHAT_DISABLE_STDOUT_LOG === "1") {
|
|
return false;
|
|
}
|
|
if (env?.MESHCHAT_FORCE_STDOUT_LOG === "1") {
|
|
return true;
|
|
}
|
|
return Boolean(proc?.stdout && proc.stdout.isTTY);
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} message
|
|
* @returns {string}
|
|
*/
|
|
function formatElectronLogLine(message) {
|
|
const text = String(message ?? "").replace(/\s+$/u, "");
|
|
return `${new Date().toISOString()} ${ELECTRON_LOG_PREFIX}${text}\n`;
|
|
}
|
|
|
|
/**
|
|
* Create a durable main-process logger that appends to meshchatx.log.
|
|
* @param {{
|
|
* getLogsDir: () => string | null | undefined,
|
|
* appendFileSync?: typeof fs.appendFileSync,
|
|
* mkdirSync?: typeof fs.mkdirSync,
|
|
* now?: () => Date,
|
|
* }} options
|
|
*/
|
|
function createMainProcessLogger(options) {
|
|
const getLogsDir = options.getLogsDir;
|
|
const appendFileSync = options.appendFileSync || fs.appendFileSync.bind(fs);
|
|
const mkdirSync = options.mkdirSync || fs.mkdirSync.bind(fs);
|
|
const pending = [];
|
|
let disabled = false;
|
|
|
|
function resolveLogPath() {
|
|
let logsDir;
|
|
try {
|
|
logsDir = getLogsDir?.();
|
|
} catch {
|
|
return null;
|
|
}
|
|
if (!logsDir || typeof logsDir !== "string") {
|
|
return null;
|
|
}
|
|
return path.join(logsDir, "meshchatx.log");
|
|
}
|
|
|
|
function flushPending() {
|
|
if (!pending.length) {
|
|
return;
|
|
}
|
|
const logPath = resolveLogPath();
|
|
if (!logPath) {
|
|
return;
|
|
}
|
|
const chunk = pending.splice(0, pending.length).join("");
|
|
try {
|
|
mkdirSync(path.dirname(logPath), { recursive: true });
|
|
appendFileSync(logPath, chunk, { encoding: "utf8" });
|
|
} catch {
|
|
disabled = true;
|
|
}
|
|
}
|
|
|
|
function writeToFile(message) {
|
|
if (disabled) {
|
|
return;
|
|
}
|
|
const line = formatElectronLogLine(message);
|
|
const logPath = resolveLogPath();
|
|
if (!logPath) {
|
|
pending.push(line);
|
|
return;
|
|
}
|
|
flushPending();
|
|
try {
|
|
mkdirSync(path.dirname(logPath), { recursive: true });
|
|
appendFileSync(logPath, line, { encoding: "utf8" });
|
|
} catch {
|
|
disabled = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Durable file write plus best-effort stdout when a TTY is present.
|
|
* @param {unknown} message
|
|
* @param {{ mirrorStdout?: boolean, proc?: NodeJS.Process, env?: NodeJS.ProcessEnv }} [opts]
|
|
*/
|
|
function write(message, opts = {}) {
|
|
writeToFile(message);
|
|
const mirror =
|
|
opts.mirrorStdout !== undefined
|
|
? opts.mirrorStdout
|
|
: shouldMirrorStdout(opts.proc || process, opts.env || process.env);
|
|
if (mirror) {
|
|
safeConsoleLog(message);
|
|
}
|
|
}
|
|
|
|
return {
|
|
write,
|
|
flushPending,
|
|
_pendingForTests: pending,
|
|
_formatLineForTests: formatElectronLogLine,
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
isBrokenPipeError,
|
|
installBrokenPipeGuards,
|
|
safeConsoleLog,
|
|
shouldMirrorStdout,
|
|
formatElectronLogLine,
|
|
createMainProcessLogger,
|
|
ELECTRON_LOG_PREFIX,
|
|
};
|