Option for eager patching

This commit is contained in:
Nuckyz 2024-05-23 06:04:21 -03:00
parent 01a4ac9c13
commit 3ab68f9296
No known key found for this signature in database
GPG key ID: 440BF8296E1C4AD9
5 changed files with 100 additions and 39 deletions

View file

@ -285,7 +285,7 @@ async function runtime(token: string) {
Object.defineProperty(navigator, "languages", { Object.defineProperty(navigator, "languages", {
get: function () { get: function () {
return ["en-US", "en"]; return ["en-US", "en"];
}, }
}); });
// Monkey patch Logger to not log with custom css // Monkey patch Logger to not log with custom css

View file

@ -32,9 +32,10 @@ export interface Settings {
autoUpdate: boolean; autoUpdate: boolean;
autoUpdateNotification: boolean, autoUpdateNotification: boolean,
useQuickCss: boolean; useQuickCss: boolean;
enableReactDevtools: boolean;
themeLinks: string[]; themeLinks: string[];
eagerPatches: boolean;
enabledThemes: string[]; enabledThemes: string[];
enableReactDevtools: boolean;
frameless: boolean; frameless: boolean;
transparent: boolean; transparent: boolean;
winCtrlQ: boolean; winCtrlQ: boolean;
@ -81,6 +82,7 @@ const DefaultSettings: Settings = {
autoUpdateNotification: true, autoUpdateNotification: true,
useQuickCss: true, useQuickCss: true,
themeLinks: [], themeLinks: [],
eagerPatches: false,
enabledThemes: [], enabledThemes: [],
enableReactDevtools: false, enableReactDevtools: false,
frameless: false, frameless: false,

View file

@ -66,6 +66,11 @@ function VencordSettings() {
title: "Enable React Developer Tools", title: "Enable React Developer Tools",
note: "Requires a full restart" note: "Requires a full restart"
}, },
{
key: "eagerPatches",
title: "Apply Vencord patches before they are needed",
note: "Increases startup timing, but may make app usage more fluid. Note that the difference of having this on or off is minimal."
},
!IS_WEB && (!IS_DISCORD_DESKTOP || !isWindows ? { !IS_WEB && (!IS_DISCORD_DESKTOP || !isWindows ? {
key: "frameless", key: "frameless",
title: "Disable the window frame", title: "Disable the window frame",

View file

@ -160,7 +160,8 @@ function initWs(isManual = false) {
return reply("Expected exactly one 'find' matches, found " + keys.length); return reply("Expected exactly one 'find' matches, found " + keys.length);
const mod = candidates[keys[0]]; const mod = candidates[keys[0]];
let src = String(mod.original ?? mod).replaceAll("\n", ""); // @ts-ignore
let src = String(mod.$$vencordOriginal ?? mod).replaceAll("\n", "");
if (src.startsWith("function(")) { if (src.startsWith("function(")) {
src = "0," + src; src = "0," + src;

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { Settings } from "@api/Settings";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { UNCONFIGURABLE_PROPERTIES } from "@utils/misc"; import { UNCONFIGURABLE_PROPERTIES } from "@utils/misc";
import { canonicalizeMatch, canonicalizeReplacement } from "@utils/patches"; import { canonicalizeMatch, canonicalizeReplacement } from "@utils/patches";
@ -16,23 +17,53 @@ import { _initWebpack, beforeInitListeners, factoryListeners, ModuleFactory, mod
const logger = new Logger("WebpackInterceptor", "#8caaee"); const logger = new Logger("WebpackInterceptor", "#8caaee");
const initCallbackRegex = canonicalizeMatch(/{return \i\(".+?"\)}/); const initCallbackRegex = canonicalizeMatch(/{return \i\(".+?"\)}/);
const allProxiedModules = new Set<WebpackRequire["m"]>();
const modulesProxyhandler: ProxyHandler<WebpackRequire["m"]> = { const modulesProxyhandler: ProxyHandler<WebpackRequire["m"]> = {
...Object.fromEntries(Object.getOwnPropertyNames(Reflect).map(propName => ...Object.fromEntries(Object.getOwnPropertyNames(Reflect).map(propName =>
[propName, (...args: any[]) => Reflect[propName](...args)] [propName, (...args: any[]) => Reflect[propName](...args)]
)), )),
get: (target, p) => { get: (target, p) => {
const mod = Reflect.get(target, p); const propValue = Reflect.get(target, p);
// If the property is not a module id, return the value of it without trying to patch // If the property is not a number, we are not dealing with a module factory
// $$vencordOriginal means the factory is already patched, $$vencordRequired means it has already been required
// and replaced with the original
// @ts-ignore // @ts-ignore
if (mod == null || mod.$$vencordOriginal != null || Number.isNaN(Number(p))) return mod; if (propValue == null || Number.isNaN(Number(p)) || propValue.$$vencordOriginal != null || propValue.$$vencordRequired === true) {
return propValue;
}
const patchedMod = patchFactory(p, mod); // This patches factories if eagerPatches are disabled
Reflect.set(target, p, patchedMod); const patchedFactory = patchFactory(p, propValue);
Reflect.set(target, p, patchedFactory);
return patchedMod; return patchedFactory;
},
set: (target, p, newValue) => {
// $$vencordRequired means we are resetting the factory to its original after being required
// If the property is not a number, we are not dealing with a module factory
if (!Settings.eagerPatches || newValue?.$$vencordRequired === true || Number.isNaN(Number(p))) {
return Reflect.set(target, p, newValue);
}
const existingFactory = Reflect.get(target, p);
// Check if this factory is already patched
// @ts-ignore
if (existingFactory?.$$vencordOriginal === newValue) {
return true;
}
const patchedFactory = patchFactory(p, newValue);
// Modules are only patched once, so we need to set the patched factory on all the modules
for (const proxiedModules of allProxiedModules) {
Reflect.set(proxiedModules, p, patchedFactory);
}
return true;
}, },
set: (target, p, newValue) => Reflect.set(target, p, newValue),
ownKeys: target => { ownKeys: target => {
const keys = Reflect.ownKeys(target); const keys = Reflect.ownKeys(target);
for (const key of UNCONFIGURABLE_PROPERTIES) { for (const key of UNCONFIGURABLE_PROPERTIES) {
@ -63,7 +94,9 @@ Object.defineProperty(Function.prototype, "O", {
if (callback != null && initCallbackRegex.test(callback.toString())) { if (callback != null && initCallbackRegex.test(callback.toString())) {
Object.defineProperty(this, "O", { Object.defineProperty(this, "O", {
value: originalOnChunksLoaded, value: originalOnChunksLoaded,
configurable: true configurable: true,
enumerable: true,
writable: true
}); });
const wreq = this; const wreq = this;
@ -104,15 +137,17 @@ Object.defineProperty(Function.prototype, "O", {
Object.defineProperty(this, "O", { Object.defineProperty(this, "O", {
value: onChunksLoaded, value: onChunksLoaded,
configurable: true configurable: true,
enumerable: true,
writable: true
}); });
} }
}); });
// wreq.m is the webpack object containing module factories. // wreq.m is the webpack object containing module factories.
// This is pre-populated with modules, and is also populated via webpackGlobal.push // This is pre-populated with module factories, and is also populated via webpackGlobal.push
// The sentry module also has their own webpack with a pre-populated modules object, so this also targets that // The sentry module also has their own webpack with a pre-populated module factories object, so this also targets that
// We replace its prototype with our proxy, which is responsible for returning patched module factories containing our patches // We replace its prototype with our proxy, which is responsible for patching the module factories
Object.defineProperty(Function.prototype, "m", { Object.defineProperty(Function.prototype, "m", {
configurable: true, configurable: true,
@ -124,35 +159,47 @@ Object.defineProperty(Function.prototype, "m", {
logger.info("Found Webpack module factory", stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? ""); logger.info("Found Webpack module factory", stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? "");
// The new object which will contain the factories // The new object which will contain the factories
const modules = Object.assign({}, originalModules); const proxiedModules: WebpackRequire["m"] = {};
// Clear the original object so pre-populated factories are patched for (const id in originalModules) {
for (const propName in originalModules) { // If we have eagerPatches enabled we have to patch the pre-populated factories
delete originalModules[propName]; if (Settings.eagerPatches) {
proxiedModules[id] = patchFactory(id, originalModules[id]);
} else {
proxiedModules[id] = originalModules[id];
}
// Clear the original object so pre-populated factories are patched if eagerPatches are disabled
delete originalModules[id];
} }
Object.setPrototypeOf(originalModules, new Proxy(modules, modulesProxyhandler)); // @ts-ignore
originalModules.$$proxiedModules = proxiedModules;
allProxiedModules.add(proxiedModules);
Object.setPrototypeOf(originalModules, new Proxy(proxiedModules, modulesProxyhandler));
} }
Object.defineProperty(this, "m", { Object.defineProperty(this, "m", {
value: originalModules, value: originalModules,
configurable: true configurable: true,
enumerable: true,
writable: true
}); });
} }
}); });
let webpackNotInitializedLogged = false; let webpackNotInitializedLogged = false;
function patchFactory(id: PropertyKey, mod: ModuleFactory) { function patchFactory(id: PropertyKey, factory: ModuleFactory) {
for (const factoryListener of factoryListeners) { for (const factoryListener of factoryListeners) {
try { try {
factoryListener(mod); factoryListener(factory);
} catch (err) { } catch (err) {
logger.error("Error in Webpack factory listener:\n", err, factoryListener); logger.error("Error in Webpack factory listener:\n", err, factoryListener);
} }
} }
const originalMod = mod; const originalFactory = factory;
const patchedBy = new Set<string>(); const patchedBy = new Set<string>();
// Discords Webpack chunks for some ungodly reason contain random // Discords Webpack chunks for some ungodly reason contain random
@ -164,7 +211,7 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) {
// cause issues. // cause issues.
// //
// 0, prefix is to turn it into an expression: 0,function(){} would be invalid syntax without the 0, // 0, prefix is to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
let code: string = "0," + mod.toString().replaceAll("\n", ""); let code: string = "0," + factory.toString().replaceAll("\n", "");
for (let i = 0; i < patches.length; i++) { for (let i = 0; i < patches.length; i++) {
const patch = patches[i]; const patch = patches[i];
@ -179,14 +226,14 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) {
patchedBy.add(patch.plugin); patchedBy.add(patch.plugin);
const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace)); const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace));
const previousMod = mod; const previousFactory = factory;
const previousCode = code; const previousCode = code;
// We change all patch.replacement to array in plugins/index // We change all patch.replacement to array in plugins/index
for (const replacement of patch.replacement as PatchReplacement[]) { for (const replacement of patch.replacement as PatchReplacement[]) {
if (replacement.predicate && !replacement.predicate()) continue; if (replacement.predicate && !replacement.predicate()) continue;
const lastMod = mod; const lastFactory = factory;
const lastCode = code; const lastCode = code;
canonicalizeReplacement(replacement, patch.plugin); canonicalizeReplacement(replacement, patch.plugin);
@ -203,7 +250,7 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) {
if (patch.group) { if (patch.group) {
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`); logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`);
mod = previousMod; factory = previousFactory;
code = previousCode; code = previousCode;
patchedBy.delete(patch.plugin); patchedBy.delete(patch.plugin);
break; break;
@ -213,7 +260,7 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) {
} }
code = newCode; code = newCode;
mod = (0, eval)(`// Webpack Module ${String(id)} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${String(id)}`); factory = (0, eval)(`// Webpack Module ${String(id)} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${String(id)}`);
} catch (err) { } catch (err) {
logger.error(`Patch by ${patch.plugin} errored (Module id is ${String(id)}): ${replacement.match}\n`, err); logger.error(`Patch by ${patch.plugin} errored (Module id is ${String(id)}): ${replacement.match}\n`, err);
@ -254,12 +301,12 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) {
if (patch.group) { if (patch.group) {
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`); logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
mod = previousMod; factory = previousFactory;
code = previousCode; code = previousCode;
break; break;
} }
mod = lastMod; factory = lastFactory;
code = lastCode; code = lastCode;
} }
} }
@ -268,23 +315,29 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) {
} }
const patchedFactory: ModuleFactory = (module, exports, require) => { const patchedFactory: ModuleFactory = (module, exports, require) => {
// @ts-ignore
originalFactory.$$vencordRequired = true;
for (const proxiedModules of allProxiedModules) {
proxiedModules[id] = originalFactory;
}
if (wreq == null && IS_DEV) { if (wreq == null && IS_DEV) {
if (!webpackNotInitializedLogged) { if (!webpackNotInitializedLogged) {
webpackNotInitializedLogged = true; webpackNotInitializedLogged = true;
logger.error("WebpackRequire was not initialized, running modules without patches instead."); logger.error("WebpackRequire was not initialized, running modules without patches instead.");
} }
return void originalMod(module, exports, require); return void originalFactory(module, exports, require);
} }
try { try {
mod(module, exports, require); factory(module, exports, require);
} catch (err) { } catch (err) {
// Just rethrow Discord errors // Just rethrow Discord errors
if (mod === originalMod) throw err; if (factory === originalFactory) throw err;
logger.error("Error in patched module", err); logger.error("Error in patched module", err);
return void originalMod(module, exports, require); return void originalFactory(module, exports, require);
} }
// Webpack sometimes sets the value of module.exports directly, so assign exports to it to make sure we properly handle it // Webpack sometimes sets the value of module.exports directly, so assign exports to it to make sure we properly handle it
@ -297,8 +350,8 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) {
Object.defineProperty(require.c, id, { Object.defineProperty(require.c, id, {
value: require.c[id], value: require.c[id],
configurable: true, configurable: true,
writable: true, enumerable: false,
enumerable: false writable: true
}); });
return; return;
} }
@ -326,9 +379,9 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) {
} }
}; };
patchedFactory.toString = originalMod.toString.bind(originalMod); patchedFactory.toString = originalFactory.toString.bind(originalFactory);
// @ts-ignore // @ts-ignore
patchedFactory.$$vencordOriginal = originalMod; patchedFactory.$$vencordOriginal = originalFactory;
return patchedFactory; return patchedFactory;
} }