export function setupIpcDebugging(): void {
if (installed) return;
installed = true;
// Check if IPC debugging should be enabled based on saved preferences
// This runs in preload context where localStorage is available
try {
const debugModeEnabled = localStorage.getItem("debug-mode-enabled");
const featureToggles = localStorage.getItem("debug-feature-toggles");
if (debugModeEnabled === "true" && featureToggles) {
const toggles = JSON.parse(featureToggles);
if (toggles.ipcViewer === true) {
enabled = true;
}
}
} catch {
// Default to disabled if there's any error reading settings
enabled = false;
}
const originalInvoke = ipcRenderer.invoke.bind(ipcRenderer);
const originalSend = ipcRenderer.send.bind(ipcRenderer);
const originalPostMessage =
typeof ipcRenderer.postMessage === "function"
? ipcRenderer.postMessage.bind(ipcRenderer)
: undefined;
const originalOn = ipcRenderer.on.bind(ipcRenderer);
const originalOnce = ipcRenderer.once.bind(ipcRenderer);
const originalAddListener = ipcRenderer.addListener.bind(ipcRenderer);
const originalRemoveListener = ipcRenderer.removeListener.bind(ipcRenderer);
const originalOff =
typeof ipcRenderer.off === "function"
? ipcRenderer.off.bind(ipcRenderer)
: undefined;
ipcRenderer.invoke = async (channel: string, ...args: unknown[]) => {
const correlationId = generateId();
const startedAt = nowMs();
appendEvent({
correlationId,
channel,
direction: "sent",
transport: "invoke",
status: "pending",
timestamp: new Date().toISOString(),
payload: createPayload(args),
});
try {
const result = await originalInvoke(channel, ...args);
const durationMs = Math.max(0, nowMs() - startedAt);
appendEvent({
correlationId,
channel,
direction: "received",
transport: "invoke-response",
status: "fulfilled",
timestamp: new Date().toISOString(),
durationMs,
payload: createPayload(result),
});
return result;
} catch (error) {
const durationMs = Math.max(0, nowMs() - startedAt);
appendEvent({
correlationId,
channel,
direction: "received",
transport: "invoke-response",
status: "rejected",
timestamp: new Date().toISOString(),
durationMs,
payload: createPayload(
error instanceof Error
? { name: error.name, message: error.message }
: error,
),
error:
error instanceof Error
? `${error.name}: ${error.message}`
: String(error),
});
throw error;
}
};
ipcRenderer.send = (channel: string, ...args: unknown[]) => {
appendEvent({
channel,
direction: "sent",
transport: "send",
timestamp: new Date().toISOString(),
payload: createPayload(args),
});
return originalSend(channel, ...args);
};
if (originalPostMessage) {
ipcRenderer.postMessage = (
channel: string,
message: unknown,
transfer?: MessagePort[],
) => {
appendEvent({
channel,
direction: "sent",
transport: "message",
timestamp: new Date().toISOString(),
payload: createPayload({
message,
transferDescriptors: transfer?.length ?? 0,
}),
});
return originalPostMessage(channel, message, transfer);
};
}
const assignListener =
(
register: (
channel: string,
listener: RendererListener,
) => typeof ipcRenderer,
mode: "on" | "once",
) =>
(channel: string, listener: RendererListener) =>
register(channel, wrapListener(channel, listener, mode));
ipcRenderer.on = assignListener(originalOn, "on");
ipcRenderer.addListener = assignListener(originalAddListener, "on");
ipcRenderer.once = assignListener(originalOnce, "once");
ipcRenderer.removeListener = (
channel: string,
listener: RendererListener,
) => {
const wrapped = listenerMap.get(listener);
if (wrapped) {
listenerMap.delete(listener);
return originalRemoveListener(channel, wrapped);
}
return originalRemoveListener(channel, listener);
};
if (originalOff) {
ipcRenderer.off = (channel: string, listener: RendererListener) => {
const wrapped = listenerMap.get(listener);
if (wrapped) {
listenerMap.delete(listener);
return originalOff(channel, wrapped);
}
return originalOff(channel, listener);
};
}
contextBridge.exposeInMainWorld("electronDebug", {
ipc: {
maxEntries: MAX_IPC_LOG_ENTRIES,
getEvents: (): IpcLogEntry[] => collector.getEntries(),
subscribe: (callback: (entries: IpcLogEntry[]) => void) =>
collector.subscribe(callback),
clear: () => collector.clear(),
setEnabled: (value: boolean) => {
enabled = value;
if (!value) {
collector.clear();
}
},
isEnabled: () => enabled,
},
getMemoryStats: () => ipcRenderer.invoke("debug:get-memory-stats"),
});
}
Sets up IPC debugging instrumentation in the preload/renderer context. Wraps ipcRenderer methods to capture and log IPC events. Exposes
electronDebugobject with IPC viewer and memory stats.