Files
NetMesh/electron/bridges/processErrorGuards.cjs

194 lines
5.6 KiB
JavaScript
Raw Permalink Normal View History

function isNonFatalNetworkError(err) {
if (!err) return false;
// Any error with an ssh2 `level` property is a connection/auth-level error,
// never a reason to kill the entire multi-session app.
if (err.level) return true;
const candidates = [err, err.cause].filter(Boolean);
for (const candidate of candidates) {
const code = candidate.code;
// Common TCP/DNS/routing errors that can surface from Node.js sockets
// without an ssh2 `level` (e.g. proxy sockets, raw net.connect calls).
switch (code) {
case "ECONNRESET":
case "ECONNREFUSED":
case "ECONNABORTED":
case "ETIMEDOUT":
case "ENOTFOUND":
case "EHOSTUNREACH":
case "EHOSTDOWN":
case "ENETUNREACH":
case "ENETDOWN":
case "EADDRNOTAVAIL":
case "EPROTO":
case "EPERM":
return true;
default:
break;
}
// Chromium/Electron networking often rejects with a message like
// "net::ERR_NETWORK_CHANGED" but without a useful `code` property.
// These are transport failures for background fetch/update/sync work,
// not reasons to kill the whole app.
const message = String(candidate.message || "");
if (/net::ERR_(?:NETWORK_[A-Z_]+|INTERNET_DISCONNECTED|NAME_NOT_RESOLVED|CONNECTION_[A-Z_]+|ADDRESS_[A-Z_]+|SSL_[A-Z_]+|CERT_[A-Z_]+|PROXY_[A-Z_]+|TUNNEL_[A-Z_]+|SOCKS_[A-Z_]+)/.test(message)) {
return true;
}
}
return false;
}
function isBenignStreamError(err) {
const code = err?.code;
return code === "EPIPE" || code === "ERR_STREAM_DESTROYED";
}
function classifyProcessError(err, options = {}) {
const runtimeStarted = options.runtimeStarted === true;
if (isBenignStreamError(err)) {
return {
action: "ignore",
reason: "benign stream teardown",
};
}
if (isNonFatalNetworkError(err)) {
return {
action: "suppress",
reason: "non-fatal network error",
};
}
if (runtimeStarted) {
return {
action: "suppress",
reason: "runtime error after startup",
};
}
return {
action: "fatal",
reason: "startup error before app became usable",
};
}
function createProcessErrorController(options = {}) {
const captureError = typeof options.captureError === "function" ? options.captureError : () => {};
const onFatalError = typeof options.onFatalError === "function"
? options.onFatalError
: (err) => { throw err; };
const logError = typeof options.logError === "function" ? options.logError : (...args) => console.error(...args);
const logWarn = typeof options.logWarn === "function" ? options.logWarn : (...args) => console.warn(...args);
let hasShownMainWindow = false;
let pendingMainWindowStartupCount = 0;
const isRuntimeProtectionActive = () => (
hasShownMainWindow && pendingMainWindowStartupCount === 0
);
const beginMainWindowStartup = () => {
pendingMainWindowStartupCount += 1;
};
const completeMainWindowStartup = ({ windowShown = false } = {}) => {
if (pendingMainWindowStartupCount > 0) {
pendingMainWindowStartupCount -= 1;
}
if (windowShown) {
hasShownMainWindow = true;
}
};
const handleUncaughtException = (err) => {
const decision = classifyProcessError(err, {
runtimeStarted: isRuntimeProtectionActive(),
origin: "uncaughtException",
});
if (decision.action === "ignore") {
logWarn("Ignored process error:", decision.reason, err?.code || err?.message || err);
return;
}
if (decision.action === "suppress") {
if (!err?.__fromUnhandledRejection) {
captureError("uncaughtException", err);
}
logError(`Suppressed uncaught exception (${decision.reason}):`, err);
return;
}
if (!err?.__fromUnhandledRejection) {
captureError("uncaughtException", err);
}
onFatalError(err, {
origin: "uncaughtException",
decision,
reason: err,
});
};
const handleUnhandledRejection = (reason) => {
const decision = classifyProcessError(reason, {
runtimeStarted: isRuntimeProtectionActive(),
origin: "unhandledRejection",
});
if (decision.action === "ignore") {
return;
}
if (decision.action === "suppress") {
captureError("unhandledRejection", reason);
logError(`Suppressed unhandled rejection (${decision.reason}):`, reason);
return;
}
captureError("unhandledRejection", reason);
const err = reason instanceof Error ? reason : new Error(String(reason));
err.__fromUnhandledRejection = true;
onFatalError(err, {
origin: "unhandledRejection",
decision,
reason,
});
};
return {
beginMainWindowStartup,
completeMainWindowStartup,
handleUncaughtException,
handleUnhandledRejection,
isRuntimeProtectionActive,
};
}
function installProcessErrorHandlers(processObject, controller) {
if (!processObject?.on || !processObject?.removeListener) {
throw new Error("A process-like EventEmitter is required");
}
if (!controller?.handleUncaughtException || !controller?.handleUnhandledRejection) {
throw new Error("A process error controller is required");
}
processObject.on("uncaughtException", controller.handleUncaughtException);
processObject.on("unhandledRejection", controller.handleUnhandledRejection);
return () => {
processObject.removeListener("uncaughtException", controller.handleUncaughtException);
processObject.removeListener("unhandledRejection", controller.handleUnhandledRejection);
};
}
module.exports = {
classifyProcessError,
createProcessErrorController,
installProcessErrorHandlers,
isBenignStreamError,
isNonFatalNetworkError,
};