Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
071c95ab5c | ||
|
|
ec99875dec |
253
electron/bridges/mainProcessErrorGuards.test.cjs
Normal file
253
electron/bridges/mainProcessErrorGuards.test.cjs
Normal file
@@ -0,0 +1,253 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const { EventEmitter } = require("node:events");
|
||||
const {
|
||||
classifyProcessError,
|
||||
createProcessErrorController,
|
||||
installProcessErrorHandlers,
|
||||
isNonFatalNetworkError,
|
||||
} = require("./processErrorGuards.cjs");
|
||||
|
||||
test("treats Chromium ERR_NETWORK_CHANGED as non-fatal", () => {
|
||||
assert.equal(
|
||||
isNonFatalNetworkError(new Error("net::ERR_NETWORK_CHANGED")),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("treats other Chromium net::ERR_* failures as non-fatal network errors", () => {
|
||||
assert.equal(
|
||||
isNonFatalNetworkError(new Error("net::ERR_INTERNET_DISCONNECTED")),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
isNonFatalNetworkError(new Error("net::ERR_NAME_NOT_RESOLVED")),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("treats Node socket error codes as non-fatal network errors", () => {
|
||||
const err = new Error("socket reset");
|
||||
err.code = "ECONNRESET";
|
||||
assert.equal(isNonFatalNetworkError(err), true);
|
||||
|
||||
const dnsErr = new Error("dns failed");
|
||||
dnsErr.code = "ENOTFOUND";
|
||||
assert.equal(isNonFatalNetworkError(dnsErr), true);
|
||||
});
|
||||
|
||||
test("keeps non-network errors fatal", () => {
|
||||
assert.equal(
|
||||
isNonFatalNetworkError(new Error("Something else broke")),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("generic startup exceptions stay fatal before the app is up", () => {
|
||||
const result = classifyProcessError(new Error("boom"), {
|
||||
runtimeStarted: false,
|
||||
});
|
||||
|
||||
assert.equal(result.action, "fatal");
|
||||
});
|
||||
|
||||
test("generic runtime exceptions are suppressed after startup", () => {
|
||||
const result = classifyProcessError(new Error("boom"), {
|
||||
runtimeStarted: true,
|
||||
});
|
||||
|
||||
assert.equal(result.action, "suppress");
|
||||
assert.match(result.reason, /runtime/i);
|
||||
});
|
||||
|
||||
test("generic runtime promise rejections are also suppressed after startup", () => {
|
||||
const result = classifyProcessError(new Error("promise boom"), {
|
||||
runtimeStarted: true,
|
||||
origin: "unhandledRejection",
|
||||
});
|
||||
|
||||
assert.equal(result.action, "suppress");
|
||||
assert.match(result.reason, /runtime/i);
|
||||
});
|
||||
|
||||
test("controller keeps startup strict until the main window is actually shown", () => {
|
||||
const controller = createProcessErrorController();
|
||||
|
||||
controller.beginMainWindowStartup();
|
||||
assert.equal(controller.isRuntimeProtectionActive(), false);
|
||||
|
||||
controller.completeMainWindowStartup({ windowShown: true });
|
||||
assert.equal(controller.isRuntimeProtectionActive(), true);
|
||||
});
|
||||
|
||||
test("controller becomes strict again while recreating a missing main window", () => {
|
||||
const controller = createProcessErrorController();
|
||||
|
||||
controller.beginMainWindowStartup();
|
||||
controller.completeMainWindowStartup({ windowShown: true });
|
||||
assert.equal(controller.isRuntimeProtectionActive(), true);
|
||||
|
||||
controller.beginMainWindowStartup();
|
||||
assert.equal(controller.isRuntimeProtectionActive(), false);
|
||||
|
||||
controller.completeMainWindowStartup({ windowShown: false });
|
||||
assert.equal(controller.isRuntimeProtectionActive(), true);
|
||||
});
|
||||
|
||||
test("startup-period errors stay fatal while recreating the main window", () => {
|
||||
const fakeProcess = new EventEmitter();
|
||||
const fatals = [];
|
||||
const controller = createProcessErrorController({
|
||||
captureError() {},
|
||||
onFatalError(err) {
|
||||
fatals.push(err.message);
|
||||
throw err;
|
||||
},
|
||||
logError() {},
|
||||
logWarn() {},
|
||||
});
|
||||
|
||||
installProcessErrorHandlers(fakeProcess, controller);
|
||||
controller.completeMainWindowStartup({ windowShown: true });
|
||||
controller.beginMainWindowStartup();
|
||||
|
||||
assert.throws(() => {
|
||||
fakeProcess.emit("uncaughtException", new Error("recreate boom"));
|
||||
}, /recreate boom/);
|
||||
assert.deepEqual(fatals, ["recreate boom"]);
|
||||
});
|
||||
|
||||
test("fatal startup failures uninstall listeners and keep throwing", () => {
|
||||
const fakeProcess = new EventEmitter();
|
||||
const captured = [];
|
||||
const fatals = [];
|
||||
let uninstall = null;
|
||||
const controller = createProcessErrorController({
|
||||
captureError(source, err) {
|
||||
captured.push([source, err.message]);
|
||||
},
|
||||
onFatalError(err) {
|
||||
fatals.push(err.message);
|
||||
uninstall?.();
|
||||
throw err;
|
||||
},
|
||||
logError() {},
|
||||
logWarn() {},
|
||||
});
|
||||
|
||||
uninstall = installProcessErrorHandlers(fakeProcess, controller);
|
||||
|
||||
assert.throws(() => {
|
||||
fakeProcess.emit("uncaughtException", new Error("startup boom"));
|
||||
}, /startup boom/);
|
||||
assert.deepEqual(fatals, ["startup boom"]);
|
||||
assert.deepEqual(captured, [["uncaughtException", "startup boom"]]);
|
||||
assert.equal(fakeProcess.listenerCount("uncaughtException"), 0);
|
||||
assert.equal(fakeProcess.listenerCount("unhandledRejection"), 0);
|
||||
});
|
||||
|
||||
test("installed handlers suppress runtime failures after startup", () => {
|
||||
const fakeProcess = new EventEmitter();
|
||||
const captured = [];
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
const controller = createProcessErrorController({
|
||||
captureError(source, err) {
|
||||
captured.push([source, err.message]);
|
||||
},
|
||||
onFatalError(err) {
|
||||
throw err;
|
||||
},
|
||||
logError(...args) {
|
||||
errors.push(args.map(String).join(" "));
|
||||
},
|
||||
logWarn(...args) {
|
||||
warnings.push(args.map(String).join(" "));
|
||||
},
|
||||
});
|
||||
|
||||
installProcessErrorHandlers(fakeProcess, controller);
|
||||
|
||||
controller.beginMainWindowStartup();
|
||||
controller.completeMainWindowStartup({ windowShown: true });
|
||||
|
||||
fakeProcess.emit("uncaughtException", new Error("runtime boom"));
|
||||
fakeProcess.emit("unhandledRejection", new Error("runtime rejection"));
|
||||
assert.deepEqual(captured, [
|
||||
["uncaughtException", "runtime boom"],
|
||||
["unhandledRejection", "runtime rejection"],
|
||||
]);
|
||||
assert.equal(errors.some((line) => line.includes("runtime error after startup")), true);
|
||||
assert.equal(warnings.length, 0);
|
||||
});
|
||||
|
||||
test("unhandled rejection marks the forwarded error so uncaught follow-up is not double-captured", () => {
|
||||
const captured = [];
|
||||
const fatals = [];
|
||||
const controller = createProcessErrorController({
|
||||
captureError(source, err) {
|
||||
captured.push([source, err.message]);
|
||||
},
|
||||
onFatalError(err) {
|
||||
fatals.push(err);
|
||||
},
|
||||
logError() {},
|
||||
logWarn() {},
|
||||
});
|
||||
|
||||
controller.handleUnhandledRejection(new Error("startup rejection"));
|
||||
assert.equal(fatals.length, 1);
|
||||
assert.equal(fatals[0].__fromUnhandledRejection, true);
|
||||
assert.deepEqual(captured, [["unhandledRejection", "startup rejection"]]);
|
||||
|
||||
controller.handleUncaughtException(fatals[0]);
|
||||
assert.deepEqual(captured, [["unhandledRejection", "startup rejection"]]);
|
||||
});
|
||||
|
||||
test("benign stream teardown errors are ignored by the installed handlers", () => {
|
||||
const fakeProcess = new EventEmitter();
|
||||
let captureCount = 0;
|
||||
let fatalCount = 0;
|
||||
const controller = createProcessErrorController({
|
||||
captureError() {
|
||||
captureCount += 1;
|
||||
},
|
||||
onFatalError() {
|
||||
fatalCount += 1;
|
||||
},
|
||||
logError() {},
|
||||
logWarn() {},
|
||||
});
|
||||
|
||||
installProcessErrorHandlers(fakeProcess, controller);
|
||||
const err = new Error("broken pipe");
|
||||
err.code = "EPIPE";
|
||||
fakeProcess.emit("uncaughtException", err);
|
||||
|
||||
assert.equal(captureCount, 0);
|
||||
assert.equal(fatalCount, 0);
|
||||
});
|
||||
|
||||
test("controller suppresses wrapped network errors from err.cause", () => {
|
||||
const err = new Error("request failed");
|
||||
err.cause = new Error("net::ERR_NETWORK_CHANGED");
|
||||
|
||||
const result = classifyProcessError(err, {
|
||||
runtimeStarted: false,
|
||||
});
|
||||
|
||||
assert.equal(isNonFatalNetworkError(err), true);
|
||||
assert.equal(result.action, "suppress");
|
||||
});
|
||||
|
||||
test("controller suppresses ssh-style errors with a level property", () => {
|
||||
const err = new Error("connection lost before handshake");
|
||||
err.level = "client-socket";
|
||||
|
||||
const result = classifyProcessError(err, {
|
||||
runtimeStarted: false,
|
||||
});
|
||||
|
||||
assert.equal(isNonFatalNetworkError(err), true);
|
||||
assert.equal(result.action, "suppress");
|
||||
});
|
||||
193
electron/bridges/processErrorGuards.cjs
Normal file
193
electron/bridges/processErrorGuards.cjs
Normal file
@@ -0,0 +1,193 @@
|
||||
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,
|
||||
};
|
||||
@@ -36,6 +36,9 @@ let menuDeps = null;
|
||||
let electronApp = null; // Reference to Electron app for userData path
|
||||
let isQuitting = false;
|
||||
const rendererReadyCallbacksByWebContentsId = new Map();
|
||||
const rendererReadySeenByWebContentsId = new Set();
|
||||
const rendererReadyWaitersByWebContentsId = new Map();
|
||||
const unhealthyWebContentsIds = new Set();
|
||||
const DEBUG_WINDOWS = process.env.NETCATTY_DEBUG_WINDOWS === "1";
|
||||
const OAUTH_DEFAULT_WIDTH = 600;
|
||||
const OAUTH_DEFAULT_HEIGHT = 700;
|
||||
@@ -791,6 +794,128 @@ function setupDeferredShow(win, { timeoutMs = 3000, waitForRendererReady = true
|
||||
return { showOnce, markRendererReady };
|
||||
}
|
||||
|
||||
function resolveRendererReady(wcId) {
|
||||
if (!wcId) return;
|
||||
unhealthyWebContentsIds.delete(wcId);
|
||||
rendererReadySeenByWebContentsId.add(wcId);
|
||||
const cb = rendererReadyCallbacksByWebContentsId.get(wcId);
|
||||
if (cb) cb();
|
||||
const waiters = rendererReadyWaitersByWebContentsId.get(wcId);
|
||||
if (!waiters || waiters.size === 0) return;
|
||||
rendererReadyWaitersByWebContentsId.delete(wcId);
|
||||
for (const resolve of waiters) {
|
||||
try {
|
||||
resolve();
|
||||
} catch {
|
||||
// ignore waiter errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isWindowUsable(win, options = {}) {
|
||||
const requireVisible = options.requireVisible === true;
|
||||
if (!win || typeof win.isDestroyed !== "function" || win.isDestroyed()) {
|
||||
return false;
|
||||
}
|
||||
if (requireVisible) {
|
||||
if (typeof win.isVisible !== "function") return false;
|
||||
try {
|
||||
if (!win.isVisible()) return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const contents = win.webContents;
|
||||
if (!contents || typeof contents.isDestroyed !== "function" || contents.isDestroyed()) {
|
||||
return false;
|
||||
}
|
||||
const wcId = (() => {
|
||||
try {
|
||||
return contents.id;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
if (wcId && unhealthyWebContentsIds.has(wcId)) {
|
||||
return false;
|
||||
}
|
||||
if (typeof contents.isCrashed === "function") {
|
||||
try {
|
||||
if (contents.isCrashed()) return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function waitForRendererReady(win, { timeoutMs = 15000 } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const wcId = (() => {
|
||||
try {
|
||||
return win?.webContents?.id;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!win || win.isDestroyed?.() || !wcId) {
|
||||
reject(new Error("Main window is unavailable before renderer ready."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (rendererReadySeenByWebContentsId.has(wcId)) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
let timer = null;
|
||||
const cleanup = () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = null;
|
||||
try { win.removeListener("closed", handleClosed); } catch {}
|
||||
try { win.webContents?.removeListener?.("render-process-gone", handleGone); } catch {}
|
||||
const waiters = rendererReadyWaitersByWebContentsId.get(wcId);
|
||||
if (waiters) {
|
||||
waiters.delete(handleReady);
|
||||
if (waiters.size === 0) {
|
||||
rendererReadyWaitersByWebContentsId.delete(wcId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleReady = () => {
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
const handleClosed = () => {
|
||||
cleanup();
|
||||
reject(new Error("Main window closed before renderer became ready."));
|
||||
};
|
||||
const handleGone = (_event, details) => {
|
||||
cleanup();
|
||||
reject(new Error(`Renderer process exited before ready: ${details?.reason || "unknown"}`));
|
||||
};
|
||||
|
||||
let waiters = rendererReadyWaitersByWebContentsId.get(wcId);
|
||||
if (!waiters) {
|
||||
waiters = new Set();
|
||||
rendererReadyWaitersByWebContentsId.set(wcId, waiters);
|
||||
}
|
||||
waiters.add(handleReady);
|
||||
|
||||
win.once("closed", handleClosed);
|
||||
win.webContents?.once?.("render-process-gone", handleGone);
|
||||
|
||||
if (Number(timeoutMs) > 0) {
|
||||
timer = setTimeout(() => {
|
||||
cleanup();
|
||||
reject(new Error("Renderer did not report ready before timeout."));
|
||||
}, timeoutMs);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the main application window
|
||||
*/
|
||||
@@ -869,12 +994,27 @@ async function createWindow(electronModule, options) {
|
||||
|
||||
// Clear reference when the main window is destroyed
|
||||
win.on('closed', () => {
|
||||
try {
|
||||
if (win?.webContents?.id) {
|
||||
unhealthyWebContentsIds.delete(win.webContents.id);
|
||||
rendererReadySeenByWebContentsId.delete(win.webContents.id);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
if (mainWindow === win) mainWindow = null;
|
||||
});
|
||||
|
||||
// Log renderer crashes for diagnostics (skip normal clean exits)
|
||||
win.webContents.on("render-process-gone", (_event, details) => {
|
||||
if (details?.reason === "clean-exit") return;
|
||||
try {
|
||||
if (win.webContents?.id) {
|
||||
unhealthyWebContentsIds.add(win.webContents.id);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
const crashLogBridge = require("./crashLogBridge.cjs");
|
||||
crashLogBridge.captureError("render-process-gone", new Error(
|
||||
@@ -1515,8 +1655,7 @@ function registerWindowHandlers(ipcMain, nativeTheme) {
|
||||
ipcMain.on("netcatty:renderer:ready", (event) => {
|
||||
const wcId = event?.sender?.id;
|
||||
if (!wcId) return;
|
||||
const cb = rendererReadyCallbacksByWebContentsId.get(wcId);
|
||||
if (cb) cb();
|
||||
resolveRendererReady(wcId);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1606,6 +1745,8 @@ module.exports = {
|
||||
buildAppMenu,
|
||||
getMainWindow,
|
||||
getSettingsWindow,
|
||||
isWindowUsable,
|
||||
waitForRendererReady,
|
||||
setIsQuitting,
|
||||
openFallbackBrowser,
|
||||
tryOpenExternalWithFallback,
|
||||
|
||||
67
electron/bridges/windowManagerReadiness.test.cjs
Normal file
67
electron/bridges/windowManagerReadiness.test.cjs
Normal file
@@ -0,0 +1,67 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const { isWindowUsable } = require("./windowManager.cjs");
|
||||
|
||||
function createWindowStub({ destroyed = false, webContents } = {}) {
|
||||
return {
|
||||
isDestroyed() {
|
||||
return destroyed;
|
||||
},
|
||||
isVisible() {
|
||||
return true;
|
||||
},
|
||||
webContents,
|
||||
};
|
||||
}
|
||||
|
||||
test("isWindowUsable returns false when webContents is crashed", () => {
|
||||
const win = createWindowStub({
|
||||
webContents: {
|
||||
isDestroyed() {
|
||||
return false;
|
||||
},
|
||||
isCrashed() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(isWindowUsable(win), false);
|
||||
});
|
||||
|
||||
test("isWindowUsable returns true for a healthy live window", () => {
|
||||
const win = createWindowStub({
|
||||
webContents: {
|
||||
isDestroyed() {
|
||||
return false;
|
||||
},
|
||||
isCrashed() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(isWindowUsable(win), true);
|
||||
});
|
||||
|
||||
test("isWindowUsable can require a visible window", () => {
|
||||
const hiddenWin = {
|
||||
...createWindowStub({
|
||||
webContents: {
|
||||
isDestroyed() {
|
||||
return false;
|
||||
},
|
||||
isCrashed() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
}),
|
||||
isVisible() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
assert.equal(isWindowUsable(hiddenWin, { requireVisible: true }), false);
|
||||
assert.equal(isWindowUsable(hiddenWin, { requireVisible: false }), true);
|
||||
});
|
||||
@@ -20,79 +20,31 @@ if (process.env.ELECTRON_RUN_AS_NODE) {
|
||||
|
||||
// Load crash log bridge early so process-level error handlers can use it
|
||||
const crashLogBridge = require("./bridges/crashLogBridge.cjs");
|
||||
|
||||
// SSH / network errors that must never crash the process.
|
||||
// ssh2 can emit multiple 'error' events per connection (e.g. ECONNRESET followed
|
||||
// by "Connection lost before handshake"). If a listener is consumed after the first
|
||||
// event, the second becomes an uncaught exception. These are non-fatal for the app.
|
||||
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 code = err.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:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle uncaught exceptions — log all, only re-throw truly fatal ones
|
||||
process.on('uncaughtException', (err) => {
|
||||
// Skip benign stream teardown errors — don't pollute crash logs with false positives
|
||||
if (err.code === 'EPIPE' || err.code === 'ERR_STREAM_DESTROYED') {
|
||||
console.warn('Ignored stream error:', err.code);
|
||||
return;
|
||||
}
|
||||
// Non-fatal SSH/network errors: log but do NOT crash the process
|
||||
if (isNonFatalNetworkError(err)) {
|
||||
if (!err.__fromUnhandledRejection) {
|
||||
try { crashLogBridge.captureError('uncaughtException', err); } catch {}
|
||||
const {
|
||||
createProcessErrorController,
|
||||
installProcessErrorHandlers,
|
||||
} = require("./bridges/processErrorGuards.cjs");
|
||||
const processErrorController = createProcessErrorController({
|
||||
captureError(source, err) {
|
||||
try { crashLogBridge.captureError(source, err); } catch {}
|
||||
},
|
||||
onFatalError(err, context) {
|
||||
uninstallProcessErrorHandlers();
|
||||
if (context?.origin === 'unhandledRejection') {
|
||||
console.error('Unhandled rejection:', context.reason);
|
||||
} else {
|
||||
console.error('Uncaught exception:', err);
|
||||
}
|
||||
console.warn('Non-fatal uncaught exception (suppressed):', err.message);
|
||||
return;
|
||||
}
|
||||
// Skip logging if already captured by unhandledRejection handler
|
||||
if (!err.__fromUnhandledRejection) {
|
||||
try { crashLogBridge.captureError('uncaughtException', err); } catch {}
|
||||
}
|
||||
console.error('Uncaught exception:', err);
|
||||
throw err;
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
// Skip benign stream teardown errors
|
||||
const code = reason?.code;
|
||||
if (code === 'EPIPE' || code === 'ERR_STREAM_DESTROYED') return;
|
||||
// Non-fatal SSH/network errors: log but do NOT re-throw
|
||||
if (isNonFatalNetworkError(reason)) {
|
||||
try { crashLogBridge.captureError('unhandledRejection', reason); } catch {}
|
||||
console.warn('Non-fatal unhandled rejection (suppressed):', reason?.message || reason);
|
||||
return;
|
||||
}
|
||||
try { crashLogBridge.captureError('unhandledRejection', reason); } catch {}
|
||||
console.error('Unhandled rejection:', reason);
|
||||
// Re-throw to preserve fatal semantics. Mark so uncaughtException handler
|
||||
// can skip duplicate logging.
|
||||
const err = reason instanceof Error ? reason : new Error(String(reason));
|
||||
err.__fromUnhandledRejection = true;
|
||||
throw err;
|
||||
throw err;
|
||||
},
|
||||
logError(...args) {
|
||||
console.error(...args);
|
||||
},
|
||||
logWarn(...args) {
|
||||
console.warn(...args);
|
||||
},
|
||||
});
|
||||
let uninstallProcessErrorHandlers = installProcessErrorHandlers(process, processErrorController);
|
||||
|
||||
// Load Electron
|
||||
let electronModule;
|
||||
@@ -1013,6 +965,80 @@ async function createWindow() {
|
||||
return win;
|
||||
}
|
||||
|
||||
function waitForWindowToShow(win) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!win || win.isDestroyed?.()) {
|
||||
reject(new Error("Main window was destroyed before first show."));
|
||||
return;
|
||||
}
|
||||
if (win.isVisible?.()) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
try { win.removeListener("show", handleShow); } catch {}
|
||||
try { win.removeListener("closed", handleClosed); } catch {}
|
||||
try { win.webContents?.removeListener?.("render-process-gone", handleGone); } catch {}
|
||||
};
|
||||
|
||||
const handleShow = () => {
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
const handleClosed = () => {
|
||||
cleanup();
|
||||
reject(new Error("Main window closed before first show."));
|
||||
};
|
||||
const handleGone = (_event, details) => {
|
||||
cleanup();
|
||||
reject(new Error(`Renderer process exited before first show: ${details?.reason || "unknown"}`));
|
||||
};
|
||||
|
||||
win.once("show", handleShow);
|
||||
win.once("closed", handleClosed);
|
||||
win.webContents?.once?.("render-process-gone", handleGone);
|
||||
});
|
||||
}
|
||||
|
||||
let mainWindowStartupPromise = null;
|
||||
|
||||
async function createAndShowMainWindow() {
|
||||
if (mainWindowStartupPromise) return mainWindowStartupPromise;
|
||||
|
||||
mainWindowStartupPromise = (async () => {
|
||||
processErrorController.beginMainWindowStartup();
|
||||
try {
|
||||
const win = await createWindow();
|
||||
await waitForWindowToShow(win);
|
||||
void getWindowManager().waitForRendererReady(win, {
|
||||
timeoutMs: isDev ? 30000 : 15000,
|
||||
}).catch((err) => {
|
||||
console.warn("[Main] Renderer ready signal was late or missing after first show:", err?.message || err);
|
||||
});
|
||||
processErrorController.completeMainWindowStartup({ windowShown: true });
|
||||
return win;
|
||||
} catch (err) {
|
||||
processErrorController.completeMainWindowStartup({ windowShown: false });
|
||||
throw err;
|
||||
} finally {
|
||||
mainWindowStartupPromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return mainWindowStartupPromise;
|
||||
}
|
||||
|
||||
function hasUsableWindow() {
|
||||
try {
|
||||
const windowManager = getWindowManager();
|
||||
return [windowManager.getMainWindow?.(), windowManager.getSettingsWindow?.()]
|
||||
.some((win) => windowManager.isWindowUsable?.(win, { requireVisible: true }));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function showStartupError(err) {
|
||||
const title = "Netcatty";
|
||||
const code = err && typeof err === "object" ? err.code : null;
|
||||
@@ -1038,9 +1064,12 @@ if (!gotLock) {
|
||||
app.on("second-instance", () => {
|
||||
if (!focusMainWindow()) {
|
||||
// Window is missing or crashed — try to recreate it
|
||||
void createWindow().catch((err) => {
|
||||
void createAndShowMainWindow().catch((err) => {
|
||||
console.error("[Main] Failed to recreate window on second-instance:", err);
|
||||
showStartupError(err);
|
||||
if (!hasUsableWindow()) {
|
||||
try { app.quit(); } catch {}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1058,9 +1087,17 @@ if (!gotLock) {
|
||||
}
|
||||
}
|
||||
|
||||
// Build and set application menu
|
||||
const menu = getWindowManager().buildAppMenu(Menu, app, isMac);
|
||||
Menu.setApplicationMenu(menu);
|
||||
// Build and set application menu. A broken menu should not take down
|
||||
// the entire app — fall back to no custom menu and continue startup.
|
||||
try {
|
||||
const menu = getWindowManager().buildAppMenu(Menu, app, isMac);
|
||||
Menu.setApplicationMenu(menu);
|
||||
} catch (err) {
|
||||
console.error("[Main] Failed to build application menu:", err);
|
||||
try {
|
||||
Menu.setApplicationMenu(null);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
app.on("browser-window-created", (_event, win) => {
|
||||
try {
|
||||
@@ -1080,7 +1117,7 @@ if (!gotLock) {
|
||||
});
|
||||
|
||||
// Create the main window
|
||||
void createWindow().then(() => {
|
||||
void createAndShowMainWindow().then(() => {
|
||||
// Trigger auto-update check 5 s after window creation.
|
||||
// startAutoCheck() is a no-op on unsupported platforms (Linux deb/rpm/snap).
|
||||
getAutoUpdateBridge().startAutoCheck(5000);
|
||||
@@ -1130,9 +1167,12 @@ if (!gotLock) {
|
||||
|
||||
if (focusMainWindow()) return;
|
||||
// Main window doesn't exist — create it even if other windows (e.g. settings) are open
|
||||
void createWindow().catch((err) => {
|
||||
void createAndShowMainWindow().catch((err) => {
|
||||
console.error("[Main] Failed to create window on activate:", err);
|
||||
showStartupError(err);
|
||||
if (!hasUsableWindow()) {
|
||||
try { app.quit(); } catch {}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
163
package-lock.json
generated
163
package-lock.json
generated
@@ -1105,13 +1105,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/xml-builder": {
|
||||
"version": "3.972.4",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.4.tgz",
|
||||
"integrity": "sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q==",
|
||||
"version": "3.972.18",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.18.tgz",
|
||||
"integrity": "sha512-BMDNVG1ETXRhl1tnisQiYBef3RShJ1kfZA7x7afivTFMLirfHNTb6U71K569HNXhSXbQZsweHvSDZ6euBw8hPA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^4.12.0",
|
||||
"fast-xml-parser": "5.3.4",
|
||||
"@smithy/types": "^4.14.1",
|
||||
"fast-xml-parser": "5.5.8",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1158,7 +1158,6 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -1804,6 +1803,7 @@
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cross-dirname": "^0.1.0",
|
||||
"debug": "^4.3.4",
|
||||
@@ -1825,6 +1825,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
@@ -1841,6 +1842,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
@@ -1855,6 +1857,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
@@ -3310,7 +3313,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz",
|
||||
"integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.9",
|
||||
"ajv": "^8.17.1",
|
||||
@@ -5594,9 +5596,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/types": {
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.12.0.tgz",
|
||||
"integrity": "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==",
|
||||
"version": "4.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz",
|
||||
"integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
@@ -6106,6 +6108,66 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "1.7.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.1.0",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "1.7.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.1.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1",
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
|
||||
@@ -6299,7 +6361,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
|
||||
"integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/unist": "*"
|
||||
}
|
||||
@@ -6380,7 +6441,6 @@
|
||||
"integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.54.0",
|
||||
@@ -6410,7 +6470,6 @@
|
||||
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.54.0",
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
@@ -6961,7 +7020,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -7012,7 +7070,6 @@
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -7573,7 +7630,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -8316,7 +8372,8 @@
|
||||
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "10.1.0",
|
||||
@@ -8600,7 +8657,6 @@
|
||||
"integrity": "sha512-uOOBA3f+kW3o4KpSoMQ6SNpdXU7WtxlJRb9vCZgOvqhTz4b3GjcoWKstdisizNZLsylhTMv8TLHFPFW0Uxsj/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "26.7.0",
|
||||
"builder-util": "26.4.1",
|
||||
@@ -8982,6 +9038,7 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@electron/asar": "^3.2.1",
|
||||
"debug": "^4.1.1",
|
||||
@@ -9002,6 +9059,7 @@
|
||||
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
"jsonfile": "^4.0.0",
|
||||
@@ -9231,7 +9289,6 @@
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -9683,10 +9740,10 @@
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz",
|
||||
"integrity": "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==",
|
||||
"node_modules/fast-xml-builder": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz",
|
||||
"integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -9695,7 +9752,24 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"strnum": "^2.1.0"
|
||||
"path-expression-matcher": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "5.5.8",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz",
|
||||
"integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-xml-builder": "^1.1.4",
|
||||
"path-expression-matcher": "^1.2.0",
|
||||
"strnum": "^2.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"fxparser": "src/cli/cli.js"
|
||||
@@ -10593,7 +10667,6 @@
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz",
|
||||
"integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
@@ -12083,7 +12156,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/debug": "^4.0.0",
|
||||
"debug": "^4.0.0",
|
||||
@@ -12701,8 +12773,7 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
@@ -12957,6 +13028,7 @@
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
@@ -12969,7 +13041,6 @@
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
|
||||
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"dompurify": "3.2.7",
|
||||
"marked": "14.0.0"
|
||||
@@ -13551,6 +13622,21 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-expression-matcher": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz",
|
||||
"integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
@@ -13729,6 +13815,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"commander": "^9.4.0"
|
||||
},
|
||||
@@ -13746,6 +13833,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || >=14"
|
||||
}
|
||||
@@ -13936,7 +14024,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -13946,7 +14033,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -15155,9 +15241,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/strnum": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz",
|
||||
"integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==",
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz",
|
||||
"integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -15277,6 +15363,7 @@
|
||||
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mkdirp": "^0.5.1",
|
||||
"rimraf": "~2.6.2"
|
||||
@@ -15341,6 +15428,7 @@
|
||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
@@ -15415,7 +15503,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -15530,7 +15617,6 @@
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
@@ -15629,7 +15715,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -15650,7 +15735,6 @@
|
||||
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
|
||||
"integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0",
|
||||
"bail": "^2.0.0",
|
||||
@@ -15989,7 +16073,6 @@
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -16083,7 +16166,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -16362,7 +16444,6 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user