Files
rea/scripts/electron-active-hook.cjs
T
360626434d fix(electron): delegate once dispatch to the active runtime (#4605)
* fix(electron): delegate once dispatch to the native runtime

Preserve the selected runtime callback dispatch and metadata semantics, recording actual entry or explicit observation gaps. Add paired cross-runtime controls and verify the real application hook source identity.

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>

* refactor(electron): remove superseded once dispatch branches

The native once runtime owns callable-method selection. Both remaining observation-helper callers pass actual invocation frames, so remove the obsolete fifth argument and its unreachable call/apply dispatch paths.

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>

* fix(electron): retain once gaps when registration bypasses on

Keep the unobserved synchronous registration scope with its owning channel and restore the previous scope after errors or reentry. Report the once limitation even when an application getter selects a native alias instead of patched on, while avoiding duplicate same-channel gaps.

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>

* docs(electron): preserve native once verification cohorts

Record paired runtime controls, actual Joplin verification, and the unresolved startup, cleanup, and observation limits.

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>

* fix(electron): retain once observation gaps on failed operations

Finalize unobserved registration scopes for both success and failure, preserving native errors and recording failed non-callable dispatch metadata.

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>

* docs(electron): record failed once operation regressions

Preserve review findings, red/green controls, final runtime comparisons and real application limits without relabeling earlier cohorts.

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>

---------

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-17 06:38:13 +08:00

1039 lines
35 KiB
JavaScript

"use strict";
const applyFunction = Reflect.apply;
const applyArguments = Function.prototype.apply;
const bindFunction = Function.prototype.bind;
const callFunction = Function.prototype.call;
const arrayFrom = Array.from;
const defineProperty = Object.defineProperty;
const ownPropertyDescriptor = Object.getOwnPropertyDescriptor;
const getPrototypeOf = Object.getPrototypeOf;
const nativeFunctionPrototype = getPrototypeOf(callFunction);
const getProperty = Reflect.get;
const setProperty = Reflect.set;
const SetConstructor = Set;
const ProxyConstructor = Proxy;
const bindBuiltinMethod = (method) =>
applyFunction(bindFunction, callFunction, [method]);
const forEachMapEntry = bindBuiltinMethod(Map.prototype.forEach);
const sliceArguments = bindBuiltinMethod(Array.prototype.slice);
const weakMapHas = bindBuiltinMethod(WeakMap.prototype.has);
const weakMapGet = bindBuiltinMethod(WeakMap.prototype.get);
const weakMapSet = bindBuiltinMethod(WeakMap.prototype.set);
const setHas = bindBuiltinMethod(Set.prototype.has);
const setAdd = bindBuiltinMethod(Set.prototype.add);
const hasOwnProperty = bindBuiltinMethod(Object.prototype.hasOwnProperty);
const { isProxy } = require("node:util").types;
const {
createElectronActiveBoundaryPatches,
} = require("./electron-active-hook-boundaries.cjs");
const ipcEventKinds = new SetConstructor([
"main-handler-invocation",
"main-event-invocation",
"utility-process-fork",
"utility-process-message",
"ipc-main-to-renderer",
"ipc-utility-to-main",
"ipc-renderer-send",
"ipc-renderer-invoke",
"ipc-renderer-post-message",
]);
const events = [];
// Budget serialized data and estimated retained object/array storage together.
const retentionBudgetBytes = 16 * 1024 * 1024;
const snapshotEnvelopeReserveBytes = 1024;
let estimatedRetainedBytes = 0;
let serializedBytes = 0;
let observed = 0;
let observedIpc = 0;
let observedRuntime = 0;
let dropped = 0;
let droppedIpc = 0;
let droppedRuntime = 0;
const droppedFamilies = new Map();
const droppedRoles = new Map();
let sequence = 0;
let correlationSequence = 0;
const shape = (value, depth = 0) => {
if (depth > 2 || value === null) return value === null ? "null" : "nested";
if (Array.isArray(value)) return "array";
switch (typeof value) {
case "string":
return "string";
case "number":
return "number";
case "boolean":
return "boolean";
case "undefined":
return "undefined";
case "function":
return "function";
case "object":
return "object";
default:
return "unknown";
}
};
// Measure JSON strings without allocating an encoded copy of a caller value.
const jsonStringBytes = (value) => {
let bytes = 2;
for (let index = 0; index < value.length; index += 1) {
const code = value.charCodeAt(index);
if (code === 0x22 || code === 0x5c) bytes += 2;
else if (
code === 0x08 ||
code === 0x09 ||
code === 0x0a ||
code === 0x0c ||
code === 0x0d
)
bytes += 2;
else if (code <= 0x1f) bytes += 6;
else if (code >= 0xd800 && code <= 0xdbff) {
const next = value.charCodeAt(index + 1);
if (next >= 0xdc00 && next <= 0xdfff) {
bytes += 4;
index += 1;
} else bytes += 6;
} else if (code >= 0xdc00 && code <= 0xdfff) bytes += 6;
else if (code <= 0x7f) bytes += 1;
else if (code <= 0x7ff) bytes += 2;
else bytes += 3;
if (bytes > retentionBudgetBytes) return bytes;
}
return bytes;
};
const jsonScalarBytes = (value) => {
if (value === null) return 4;
if (typeof value === "string") return jsonStringBytes(value);
if (typeof value === "boolean") return value ? 4 : 5;
return String(value).length;
};
const eventFamily = (kind, processTypeValue) => {
if (
kind === "preload" &&
(processTypeValue === "main" || processTypeValue === "browser")
)
return "preload-configuration";
if (kind.startsWith("ipc-renderer"))
return processTypeValue === "renderer" ? "renderer-ipc" : "ipc";
return setHas(ipcEventKinds, kind) ? "ipc" : kind;
};
const noteDropped = (kind, processTypeValue, isIpc) => {
dropped += 1;
if (isIpc) droppedIpc += 1;
else droppedRuntime += 1;
const family = eventFamily(kind, processTypeValue);
droppedFamilies.set(family, (droppedFamilies.get(family) ?? 0) + 1);
if (typeof processTypeValue === "string")
droppedRoles.set(
processTypeValue,
(droppedRoles.get(processTypeValue) ?? 0) + 1,
);
if (kind === "window-lifecycle")
droppedRoles.set("window", (droppedRoles.get("window") ?? 0) + 1);
if (kind === "web-contents-lifecycle" || kind === "navigation")
droppedRoles.set(
"web_contents",
(droppedRoles.get("web_contents") ?? 0) + 1,
);
if (kind === "preload" && processTypeValue === "preload")
droppedRoles.set("preload", (droppedRoles.get("preload") ?? 0) + 1);
};
const isAllowedNavigation = (value) => {
if (typeof value !== "string") return true;
try {
const parsed = new URL(value);
if (["file:", "data:", "about:"].includes(parsed.protocol)) return true;
return (
["http:", "https:"].includes(parsed.protocol) &&
["127.0.0.1", "localhost", "[::1]", "::1"].includes(parsed.hostname)
);
} catch {
return false;
}
};
const processType = () =>
typeof process.type === "string" && process.type.length > 0
? process.type
: "main";
const identity = (value, prefix) => {
if (value === null || value === undefined) return null;
const id = value.id;
const processId = value.pid;
return typeof id === "string" || typeof id === "number"
? `${prefix}:${String(id)}`
: typeof processId === "number"
? `${prefix}:pid:${String(processId)}`
: null;
};
const frameIdentity = (event) => {
const frame = event?.senderFrame ?? event?.frame;
const id = frame?.frameTreeNodeId ?? frame?.routingId ?? frame?.id;
return typeof id === "string" || typeof id === "number"
? `frame:${String(id)}`
: null;
};
const record = (event) => {
observed += 1;
const isIpc = setHas(ipcEventKinds, event.kind);
if (isIpc) observedIpc += 1;
else observedRuntime += 1;
const raw = {
sequence: ++sequence,
correlation_id:
typeof event.correlation_id === "string"
? event.correlation_id
: `capture:${process.pid}:${++correlationSequence}`,
event: null,
phase: "observed",
channel: null,
direction: null,
sender: null,
receiver: null,
frame: null,
target: null,
argument_shapes: [],
result_shape: null,
process_type: processType(),
source: "electron-active-hook",
capture_method: "api-wrapper",
artifact_path: null,
artifact_sha256: null,
error: false,
...event,
};
const shapeGroups = Array.isArray(raw.argument_shape_value_groups)
? raw.argument_shape_value_groups
: Array.isArray(raw.argument_shape_values)
? [raw.argument_shape_values]
: [];
const existingShapes = Array.isArray(raw.argument_shapes)
? raw.argument_shapes
: [];
const retainedEvent = {
...raw,
correlation_id: raw.correlation_id,
event: typeof raw.event === "string" ? raw.event : null,
channel: typeof raw.channel === "string" ? raw.channel : null,
sender: typeof raw.sender === "string" ? raw.sender : null,
receiver: typeof raw.receiver === "string" ? raw.receiver : null,
frame: typeof raw.frame === "string" ? raw.frame : null,
target: typeof raw.target === "string" ? raw.target : null,
argument_shapes: [],
result_shape:
typeof raw.result_shape === "string" ? raw.result_shape : null,
process_type:
typeof raw.process_type === "string" ? raw.process_type : null,
artifact_path:
typeof raw.artifact_path === "string" ? raw.artifact_path : null,
artifact_sha256:
typeof raw.artifact_sha256 === "string" ? raw.artifact_sha256 : null,
};
delete retainedEvent.argument_shape_values;
delete retainedEvent.argument_shape_value_groups;
// All event values are JSON scalars except argument_shapes. Measure them
// incrementally so large channels, paths, or shape lists never require a
// full JSON.stringify temporary allocation before the budget decision.
let eventBytes = 2;
let argumentShapeCount = 0;
const eventKeys = Object.keys(retainedEvent);
for (let index = 0; index < eventKeys.length; index += 1) {
const key = eventKeys[index];
const value = retainedEvent[key];
if (index > 0) eventBytes += 1;
eventBytes += jsonStringBytes(key) + 1;
if (key === "argument_shapes") {
eventBytes += 2;
let shapeOverflow = false;
const countShape = (argument) => {
const encodedShapeBytes = jsonStringBytes(shape(argument));
eventBytes += encodedShapeBytes + (argumentShapeCount === 0 ? 0 : 1);
argumentShapeCount += 1;
shapeOverflow =
eventBytes + estimatedRetainedBytes + snapshotEnvelopeReserveBytes >
retentionBudgetBytes;
};
for (const group of shapeGroups) {
if (!Array.isArray(group)) continue;
for (const argument of group) {
countShape(argument);
if (shapeOverflow) break;
}
}
if (shapeOverflow) {
noteDropped(raw.kind, retainedEvent.process_type, isIpc);
return null;
}
for (const valueShape of existingShapes)
if (!shapeOverflow) {
eventBytes +=
jsonStringBytes(
typeof valueShape === "string" ? valueShape : "unknown",
) + (argumentShapeCount++ === 0 ? 0 : 1);
shapeOverflow =
eventBytes + estimatedRetainedBytes + snapshotEnvelopeReserveBytes >
retentionBudgetBytes;
}
if (shapeOverflow) {
noteDropped(raw.kind, retainedEvent.process_type, isIpc);
return null;
}
} else eventBytes += jsonScalarBytes(value);
if (
eventBytes + estimatedRetainedBytes + snapshotEnvelopeReserveBytes >
retentionBudgetBytes
) {
noteDropped(raw.kind, retainedEvent.process_type, isIpc);
return null;
}
}
// Reserve room for the only fields mutated after recording: result_shape and
// error. The shape vocabulary's longest value is "undefined".
const reservedMutationBytes = retainedEvent.result_shape === null ? 7 : 0;
// Include one byte for the event-array separator and reserve space for the
// snapshot's counters and JSON delimiters.
// One retained event, one defensive snapshot copy, and up to two serialized
// result views (timeline plus IPC), with per-object and per-array-slot costs.
const eventEstimateBytes = eventBytes * 3 + 512 + argumentShapeCount * 16;
const retainedEventBytes = eventEstimateBytes + reservedMutationBytes + 1;
if (
estimatedRetainedBytes + retainedEventBytes + snapshotEnvelopeReserveBytes >
retentionBudgetBytes
) {
noteDropped(raw.kind, retainedEvent.process_type, isIpc);
return null;
}
for (const group of shapeGroups) {
if (!Array.isArray(group)) continue;
for (const argument of group)
retainedEvent.argument_shapes.push(shape(argument));
}
for (const valueShape of existingShapes)
retainedEvent.argument_shapes.push(
typeof valueShape === "string" ? valueShape : "unknown",
);
events.push(retainedEvent);
estimatedRetainedBytes += retainedEventBytes;
serializedBytes += eventBytes + reservedMutationBytes + 1;
return retainedEvent;
};
const recordRuntime = (kind, event, phase, details = {}) =>
record({ kind, event, phase, ...details });
const recordIpc = (kind, channel, args, details = {}) =>
(() => {
const values = Array.isArray(args) ? args : [];
return record({
kind,
channel: typeof channel === "string" ? channel : null,
argument_shape_values: values,
...details,
});
})();
const recordIpcObservationGap = (channel, event, error = false) =>
recordRuntime("ipc-observation-gap", event, error ? "failed" : "observed", {
channel,
target: "ipcMain.on",
process_type: "main",
error,
});
const patchEmitter = (emitter, observeEvent) => {
if (emitter === null || emitter === undefined) return;
const original = emitter.emit;
if (typeof original !== "function") return;
if (emitter.__reaElectronActiveEmitPatched === true) return;
try {
emitter.__reaElectronActiveEmitPatched = true;
emitter.emit = function patchedEmit(event, ...args) {
if (typeof event === "string") observeEvent.call(this, event, args);
return original.call(this, event, ...args);
};
} catch {
globalThis.__reaElectronActiveHookError = true;
}
};
const recordInvocation = (kind, channel, event, args) =>
recordIpc(kind, channel, args, {
direction: "renderer-to-main",
sender: identity(event?.sender, "webContents"),
receiver: "main",
frame: frameIdentity(event),
process_type: "main",
error: false,
});
const invokeObservedEventListener = (thisArg, channel, listener, args) => {
// Recording slices an actual invocation frame, avoiding Array species;
// callback dispatch keeps its native Array/Arguments representation.
const recorded = recordInvocation(
"main-event-invocation",
channel,
args[0],
sliceArguments(args, 1),
);
try {
return applyFunction(listener, thisArg, args);
} catch (cause) {
if (recorded !== null) recorded.error = true;
throw cause;
}
};
// Recording wrappers follow EventEmitter's own wrapper convention: `.listener`
// names the caller's function so native removal can match it. Node retains
// ownership of listener traversal and meta-events; hook-created "on" entries
// are translated before calling the application's removal method/decorators.
// Only hook-created wrappers are tracked, by the registration they stand in
// for: they already record, so patched on() registers them as is. An "on"
// wrapper stands in for the caller's raw function and a "once" wrapper for
// EventEmitter's once wrapper, which removeAllListeners reports accordingly.
// The value is the native removal argument: the app function for an "on"
// wrapper, or the wrapper itself for "once". Public .listener is mutable;
// it is not the authority for the original function's identity.
const hookListenerRemovalTargets = new WeakMap();
const MAX_LISTENER_CHAIN_HOPS = 16;
// Parse native property lookup without invoking application getters or Proxy
// traps. Undefined means absent; null means the lookup is unknown.
const lookupPropertyDescriptor = (listener, key) => {
let candidate = listener;
for (
let hops = 0;
hops <= MAX_LISTENER_CHAIN_HOPS && candidate !== null;
hops += 1
) {
if (isProxy(candidate)) return null;
const descriptor = ownPropertyDescriptor(candidate, key);
if (descriptor !== undefined) return descriptor;
candidate = getPrototypeOf(candidate);
}
return candidate === null ? undefined : null;
};
const listenerDataDescriptor = (listener) => {
const descriptor = lookupPropertyDescriptor(listener, "listener");
if (descriptor === null || descriptor === undefined) return descriptor;
if (hasOwnProperty(descriptor, "value")) return descriptor;
// A setter-only property shadows ancestors with a known undefined read.
return descriptor.get === undefined ? undefined : null;
};
// A decorator that wraps once's hook wrapper and names it through `.listener`
// already records through that wrapper. Registering the decorator as is keeps
// native self-removal working: the once wrapper removes itself by identity, and
// removeListener matches the decorator through its `.listener`.
const classifyHookListenerWrapper = (listener) => {
const visited = new SetConstructor();
let candidate = listener;
for (
let hops = 0;
hops <= MAX_LISTENER_CHAIN_HOPS && typeof candidate === "function";
hops += 1
) {
if (weakMapHas(hookListenerRemovalTargets, candidate)) return "owned";
if (setHas(visited, candidate)) return "unowned";
setAdd(visited, candidate);
const descriptor = listenerDataDescriptor(candidate);
if (descriptor === null) return "unknown";
candidate = descriptor?.value;
}
return typeof candidate === "function" ? "unknown" : "unowned";
};
const createObservedOnceCallback = (target, channel, listener, receiver) => {
// A private callable target avoids Proxy get invariants on an app's
// non-configurable, read-only call/apply properties. It never escapes
// through the native once wrapper's public listener link.
const callback = new ProxyConstructor(
function observedOnceListener() {
return invokeObservedEventListener(
this === receiver ? target : this,
channel,
listener,
arguments,
);
},
{
get: (_callable, key) => {
let value;
try {
value = getProperty(listener, key, listener);
} catch (cause) {
if (key === "call" || key === "apply")
recordIpcObservationGap(channel, `custom-${key}`, true);
throw cause;
}
if (key !== "call" && key !== "apply") return value;
if (typeof value !== "function") {
recordIpcObservationGap(channel, `custom-${key}`, true);
return value;
}
if (value === callFunction || value === applyArguments) return value;
// A runtime that selects an application method must preserve that
// method's receiver and original argument-list object. Its body may
// bypass the callable Proxy, so retain a gap rather than an invocation.
return new ProxyConstructor(value, {
apply: (method, thisArg, args) => {
const gap = recordIpcObservationGap(channel, `custom-${key}`);
if (args[0] === receiver) args[0] = target;
try {
return applyFunction(
method,
thisArg === callback ? listener : thisArg,
args,
);
} catch (cause) {
if (gap !== null) {
gap.error = true;
gap.phase = "failed";
}
throw cause;
}
},
});
},
},
);
return callback;
};
const patchIpcMain = (ipcMain) => {
if (ipcMain === null || ipcMain === undefined) return;
const { handle, on, once, removeAllListeners } = ipcMain;
let unobservedOnceScope = null;
if (typeof handle === "function")
ipcMain.handle = function patchedIpcMainHandle(channel, listener) {
if (typeof listener !== "function")
return handle.call(this, channel, listener);
return handle.call(
this,
channel,
async function wrappedHandler(event, ...args) {
const recorded = recordInvocation(
"main-handler-invocation",
channel,
event,
args,
);
try {
const result = await applyFunction(listener, this, arguments);
if (recorded !== null) recorded.result_shape = shape(result);
return result;
} catch (cause) {
if (recorded !== null) recorded.error = true;
throw cause;
}
},
);
};
if (typeof on !== "function") return;
ipcMain.on = function patchedIpcMainOn(channel, listener) {
if (typeof listener !== "function") return on.call(this, channel, listener);
if (unobservedOnceScope !== null) {
const scope = unobservedOnceScope;
const result = on.call(this, channel, listener);
recordIpcObservationGap(channel, "once-metadata");
if (scope.channel === channel) scope.gapRecorded = true;
return result;
}
const wrapperStatus = classifyHookListenerWrapper(listener);
if (wrapperStatus !== "unowned") {
const result = on.call(this, channel, listener);
if (wrapperStatus === "unknown")
recordIpcObservationGap(channel, "listener-chain");
return result;
}
const wrapped = function wrappedEventListener() {
return invokeObservedEventListener(this, channel, listener, arguments);
};
// Native single-listener dispatch reads .apply; array dispatch invokes the
// function directly. Let Node select that path without inspecting or
// copying the emitter's listener storage, including during reentrant emits.
defineProperty(wrapped, "apply", {
configurable: true,
writable: true,
value: function nativeApply(...methodArgs) {
let applicationApply;
try {
applicationApply = listener.apply;
} catch (cause) {
recordIpcObservationGap(channel, "custom-apply", true);
throw cause;
}
if (applicationApply !== applyArguments) {
// Custom apply owns the original list's interpretation. Forward it
// unchanged rather than executing getters to invent callback facts.
const gap = recordIpcObservationGap(channel, "custom-apply");
try {
return applyFunction(
applicationApply,
this === wrapped ? listener : this,
methodArgs,
);
} catch (cause) {
if (gap !== null) {
gap.error = true;
gap.phase = "failed";
}
throw cause;
}
}
// Native Function.apply performs its own list conversion before the
// recording wrapper observes an actual invocation frame.
// Borrowing the exposed method keeps its dynamic receiver.
return applyFunction(applyArguments, this, methodArgs);
},
});
defineProperty(wrapped, "listener", {
configurable: true,
enumerable: true,
writable: true,
value: listener,
});
weakMapSet(hookListenerRemovalTargets, wrapped, listener);
return on.call(this, channel, wrapped);
};
// Native once owns firing, removal and callback dispatch. Node versions
// differ in whether they read callback.call/apply. Observe actual invocation
// through a private callable Proxy instead of copying either implementation.
if (typeof once === "function")
ipcMain.once = function patchedIpcMainOnce(channel, listener) {
if (
typeof listener !== "function" ||
this === null ||
(typeof this !== "object" && typeof this !== "function")
)
return once.call(this, channel, listener);
const target = this;
const metadata = lookupPropertyDescriptor(
nativeFunctionPrototype,
"listener",
);
const onMetadata = lookupPropertyDescriptor(target, "on");
if (
metadata === null ||
(metadata !== undefined && !hasOwnProperty(metadata, "value")) ||
onMetadata === null ||
(onMetadata !== undefined && !hasOwnProperty(onMetadata, "value"))
) {
// Native wrapper creation can expose its callback through an inherited
// setter/Proxy, including one installed by an on getter. Pass the exact
// app callback; registrations in this synchronous scope stay unwrapped.
const previousScope = unobservedOnceScope;
const scope = { channel, gapRecorded: false };
let failed = true;
unobservedOnceScope = scope;
try {
const result = applyFunction(once, target, [channel, listener]);
failed = false;
return result;
} finally {
unobservedOnceScope = previousScope;
// An app decorator can register through an unhooked alias and then
// throw. Preserve a gap for either outcome, not proof of registration.
if (!scope.gapRecorded)
recordIpcObservationGap(channel, "once-metadata", failed);
}
}
const receiver = new ProxyConstructor(
{ __proto__: null },
{
get: (_target, key) => {
const value = getProperty(target, key, target);
if (
(key !== "on" && key !== "removeListener") ||
typeof value !== "function"
)
return value;
return function forwardOnceOperation(...args) {
if (key === "on") {
const wrapped = args[1];
defineProperty(wrapped, "listener", {
...ownPropertyDescriptor(wrapped, "listener"),
value: listener,
});
weakMapSet(hookListenerRemovalTargets, wrapped, wrapped);
}
return applyFunction(value, target, args);
};
},
set: (_target, key, value) => setProperty(target, key, value, target),
},
);
const callback = createObservedOnceCallback(
target,
channel,
listener,
receiver,
);
const result = applyFunction(once, receiver, [channel, callback]);
return result === receiver ? target : result;
};
if (typeof removeAllListeners !== "function") return;
// Node owns its live-array traversal. A forwarding receiver translates only
// our "on" entries before calling the current application removal method,
// including decorators installed/replaced during traversal. Application
// getters and methods always receive the real emitter; its public methods
// are never temporarily replaced. Native once entries remain unchanged.
ipcMain.removeAllListeners = function patchedIpcMainRemoveAllListeners(
...args
) {
const emitter = this;
// A separate empty target also permits forwarding a non-writable app
// method; a Proxy of the emitter itself would violate get invariants.
const receiver = new ProxyConstructor(
{ __proto__: null },
{
get(_target, key) {
const value = getProperty(emitter, key, emitter);
if (
(key !== "removeListener" && key !== "removeAllListeners") ||
typeof value !== "function"
)
return value;
return (...methodArgs) => {
if (key === "removeListener") {
const entry = methodArgs[1];
const removalTarget = weakMapGet(
hookListenerRemovalTargets,
entry,
);
// Translate only a still-valid data link. If app code changed it
// to an accessor, native removal owns when that getter executes.
if (
removalTarget !== undefined &&
removalTarget !== entry &&
listenerDataDescriptor(entry)?.value === removalTarget
)
methodArgs[1] = removalTarget;
}
return applyFunction(value, emitter, methodArgs);
};
},
set: (_target, key, value) => setProperty(emitter, key, value, emitter),
},
);
const result = applyFunction(removeAllListeners, receiver, args);
return result === receiver ? emitter : result;
};
};
const patchNavigationMethods = (webContents, contentsId) => {
for (const method of ["loadURL", "loadFile", "loadDataURL"]) {
const original = webContents[method];
if (typeof original !== "function") continue;
webContents[method] = function patchedNavigation(...args) {
recordRuntime("navigation", method, "attempted", {
target: contentsId,
argument_shape_values: args,
});
if (method === "loadURL" && !isAllowedNavigation(args[0])) {
recordRuntime("navigation", method, "blocked", {
target: contentsId,
error: true,
});
return Promise.reject(new Error("External navigation is blocked"));
}
try {
const result = original.apply(this, args);
if (result !== null && typeof result?.then === "function")
return result.then(
(value) => {
recordRuntime("navigation", method, "completed", {
target: contentsId,
});
return value;
},
(cause) => {
recordRuntime("navigation", method, "failed", {
target: contentsId,
error: true,
});
throw cause;
},
);
recordRuntime("navigation", method, "completed", {
target: contentsId,
});
return result;
} catch (cause) {
recordRuntime("navigation", method, "failed", {
target: contentsId,
error: true,
});
throw cause;
}
};
}
};
const patchWebContentsIpc = (webContents, contentsId) => {
const originalSend = webContents.send;
if (typeof originalSend === "function")
webContents.send = function patchedSend(channel, ...args) {
const recorded = recordIpc("ipc-main-to-renderer", channel, args, {
direction: "main-to-renderer",
sender: "main",
receiver: contentsId,
target: contentsId,
process_type: "main",
error: false,
});
try {
const result = originalSend.call(this, channel, ...args);
if (recorded !== null) recorded.result_shape = shape(result);
return result;
} catch (cause) {
if (recorded !== null) recorded.error = true;
throw cause;
}
};
const originalPostMessage = webContents.postMessage;
if (typeof originalPostMessage === "function")
webContents.postMessage = function patchedPostMessage(
channel,
message,
transfer,
) {
const recorded = recordIpc(
"ipc-main-to-renderer",
channel,
[message, transfer],
{
direction: "main-to-renderer",
sender: "main",
receiver: contentsId,
target: contentsId,
process_type: "main",
error: false,
},
);
try {
const result = originalPostMessage.call(
this,
channel,
message,
transfer,
);
if (recorded !== null) recorded.result_shape = shape(result);
return result;
} catch (cause) {
if (recorded !== null) recorded.error = true;
throw cause;
}
};
};
const patchWebContents = (webContents, windowId) => {
if (webContents === null || webContents === undefined) return;
const contentsId = identity(webContents, "webContents");
patchEmitter(webContents, (event, args) => {
const navigation = [
"did-start-loading",
"did-stop-loading",
"did-finish-load",
"did-fail-load",
"did-frame-finish-load",
"will-navigate",
"will-redirect",
].includes(event);
const kind = navigation
? "navigation"
: [
"console-message",
"render-process-gone",
"crashed",
"unresponsive",
"responsive",
].includes(event)
? "error"
: event === "did-create-window"
? "popup-attempt"
: "web-contents-lifecycle";
if (event === "will-navigate" || event === "will-redirect") {
if (!isAllowedNavigation(args[1])) {
const preventDefault = args[0]?.preventDefault;
if (typeof preventDefault === "function") preventDefault.call(args[0]);
recordRuntime("navigation", event, "blocked", {
target: contentsId,
sender: windowId,
argument_shape_values: args,
error: true,
});
return;
}
}
recordRuntime(kind, event, "observed", {
target: contentsId,
sender: windowId,
argument_shape_values: args,
error: kind === "error",
});
if (event === "did-create-window") {
const childWindow = args[0];
patchWebContents(
childWindow?.webContents,
identity(childWindow, "window"),
);
}
});
patchNavigationMethods(webContents, contentsId);
patchWebContentsIpc(webContents, contentsId);
boundaries.patchWindowOpenHandler(webContents, contentsId);
};
const patchWindows = (app) => {
if (app === null || app === undefined) return;
app.on("browser-window-created", (_event, window) => {
const targetId = identity(window, "window");
recordRuntime("window-lifecycle", "created", "completed", {
target: targetId,
capture_method: "event-emitter",
});
try {
// Electron 44 separates the preload descriptor from WebPreferences.
// Read the engine's descriptor, never infer a path from application code.
const preferences = window.webContents.getLastWebPreferences?.();
const preload =
typeof preferences?.preload === "string"
? preferences.preload
: window.webContents._getPreloadScript?.()?.filePath;
if (typeof preload === "string")
recordRuntime("preload", "configured", "completed", {
target: targetId,
artifact_path: preload,
});
} catch (cause) {
recordRuntime(
"error",
cause instanceof Error ? cause.message : String(cause),
"failed",
{
target: targetId,
error: true,
},
);
}
patchEmitter(window, (event) =>
recordRuntime("window-lifecycle", event, "observed", {
target: targetId,
}),
);
patchWebContents(window.webContents, targetId);
});
};
const boundaries = createElectronActiveBoundaryPatches({
identity,
patchEmitter,
processType,
recordIpc,
recordRuntime,
shape,
});
try {
const electron = require("electron");
patchEmitter(electron.app, (event, args) =>
recordRuntime(
event === "open-url" || event === "second-instance"
? "protocol"
: "app-lifecycle",
event,
"observed",
{
target: "app",
argument_shape_values: args,
process_type: "main",
},
),
);
patchIpcMain(electron.ipcMain);
boundaries.patchIpcRenderer(electron.ipcRenderer);
boundaries.patchContextBridge(electron.contextBridge);
patchWindows(electron.app);
boundaries.patchUtilityProcess(electron.utilityProcess);
boundaries.patchShell(electron.shell);
boundaries.patchSessionManager(electron.session, electron.app);
boundaries.patchApplicationEffects(electron.app);
boundaries.patchNativeAddonLoading();
boundaries.patchChildProcess();
process.once("uncaughtException", (cause, origin) => {
recordRuntime("process-lifecycle", "uncaught-exception", "observed", {
argument_shape_values: [cause, origin],
error: true,
});
throw cause;
});
process.once("unhandledRejection", (reason, promise) => {
recordRuntime("process-lifecycle", "unhandled-rejection", "observed", {
argument_shape_values: [reason, promise],
error: true,
});
throw reason;
});
} catch (cause) {
globalThis.__reaElectronActiveHookError = true;
recordRuntime(
"error",
cause instanceof Error ? cause.message : String(cause),
"failed",
{
target: "active-hook-installation",
error: true,
},
);
}
// Never expose retained buffers to application iterators or array constructors.
const snapshotArray = (values, project) =>
arrayFrom({ __proto__: null, length: values.length }, (_unused, index) =>
project === undefined ? values[index] : project(values[index]),
);
const snapshotCounts = (counts, field) => {
const values = { __proto__: null, length: 0 };
forEachMapEntry(counts, (count, value) => {
values[values.length++] = { [field]: value, count };
});
return arrayFrom(values);
};
globalThis.__reaElectronActiveSnapshot = () => ({
events: snapshotArray(events, (event) => ({
...event,
argument_shapes: snapshotArray(event.argument_shapes),
})),
retention_budget_bytes: retentionBudgetBytes,
estimated_retained_bytes: estimatedRetainedBytes,
event_serialized_byte_upper_bound: serializedBytes,
retained: events.length,
dropped,
dropped_ipc: droppedIpc,
dropped_runtime: droppedRuntime,
dropped_event_families: snapshotCounts(droppedFamilies, "family"),
dropped_event_roles: snapshotCounts(droppedRoles, "role"),
observed,
observed_ipc: observedIpc,
observed_runtime: observedRuntime,
hook_error: globalThis.__reaElectronActiveHookError === true,
});