fix: remove broken perf patches that hang kv storage, fix resource package mappings, and fix fonts hook

This commit is contained in:
Rosie Val 2026-09-25 17:43:43 -04:00
parent 3e47076257
commit 2864920cf2
4 changed files with 35 additions and 298 deletions

View file

@ -16,7 +16,13 @@ object FixResourcesModule : Module() {
"getIdentifier", String::class.java, String::class.java, String::class.java "getIdentifier", String::class.java, String::class.java, String::class.java
) { ) {
before { before {
if (args[2] == TARGET_PACKAGE) args[2] = packageName if (args[2] == packageName) {
args[2] = TARGET_PACKAGE
}
val name = args[0] as? String
if (name != null && name.startsWith("$packageName:")) {
args[0] = name.replaceFirst("$packageName:", "$TARGET_PACKAGE:")
}
} }
} }
} }

View file

@ -59,33 +59,35 @@ object HookScriptLoaderModule : Module() {
).mapNotNull { classLoader.safeLoadClass(it) }.forEach { hook(it) } ).mapNotNull { classLoader.safeLoadClass(it) }.forEach { hook(it) }
} }
private fun hook(instance: Class<*>) = runCatching { private fun hook(instance: Class<*>) {
val loadScriptFromAssets = instance.method( val loadScriptFromAssets = runCatching {
"loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType instance.method(
) "loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType
)
}.getOrNull()
val loadScriptFromFile = instance.method( val loadScriptFromFile = runCatching {
"loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType instance.method(
) "loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType
)
}.getOrNull()
loadScriptFromAssets.hook { loadScriptFromAssets?.hook {
before { before {
Log.i("Received call to loadScriptFromAssets: ${args[1]} (sync: ${args[2]})") Log.i("Received call to loadScriptFromAssets: ${args[1]} (sync: ${args[2]})")
runCustomScripts(loadScriptFromFile, loadScriptFromAssets) runCustomScripts(loadScriptFromFile, loadScriptFromAssets)
} }
} }
loadScriptFromFile.hook { loadScriptFromFile?.hook {
before { before {
Log.i("Received call to loadScriptFromFile: ${args[0]} (sync: ${args[2]})") Log.i("Received call to loadScriptFromFile: ${args[0]} (sync: ${args[2]})")
runCustomScripts(loadScriptFromFile, loadScriptFromAssets) runCustomScripts(loadScriptFromFile, loadScriptFromAssets)
} }
} }
}.onFailure {
Log.e("Failed to hook script loading methods in ${instance.name}:", it)
} }
private fun HookScope.runCustomScripts(loadScriptFromFile: Method, loadScriptFromAssets: Method) { private fun HookScope.runCustomScripts(loadScriptFromFile: Method?, loadScriptFromAssets: Method?) {
Log.i("Running custom scripts...") Log.i("Running custom scripts...")
runBlocking { runBlocking {
@ -111,9 +113,11 @@ object HookScriptLoaderModule : Module() {
val runScriptFile = { file: File -> val runScriptFile = { file: File ->
Log.i("Loading script: ${file.absolutePath}") Log.i("Loading script: ${file.absolutePath}")
XposedBridge.invokeOriginalMethod( if (loadScriptFromFile != null) {
loadScriptFromFile, thisObject, arrayOf(file.absolutePath, file.absolutePath, loadSynchronously) XposedBridge.invokeOriginalMethod(
) loadScriptFromFile, thisObject, arrayOf(file.absolutePath, file.absolutePath, loadSynchronously)
)
}
Unit Unit
} }
@ -141,7 +145,7 @@ object HookScriptLoaderModule : Module() {
resources.assets.open("Shiggy.bundle").use { hasFallback = true } resources.assets.open("Shiggy.bundle").use { hasFallback = true }
} catch (_: Throwable) {} } catch (_: Throwable) {}
if (hasFallback) { if (hasFallback && loadScriptFromAssets != null) {
Log.i("Loading fallback assets://Shiggy.bundle") Log.i("Loading fallback assets://Shiggy.bundle")
XposedBridge.invokeOriginalMethod( XposedBridge.invokeOriginalMethod(
loadScriptFromAssets, loadScriptFromAssets,

View file

@ -25,293 +25,20 @@ object PerfPatchesModule : Module() {
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) { override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {
try { try {
// Build preloads path inside the app data files directory val filesDir = File(packageParam.appInfo.dataDir, Constants.FILES_DIR)
val filesDir = File(packageParam.appInfo.dataDir, Constants.FILES_DIR).apply { mkdirs() } val preloadsDir = File(filesDir, HookScriptLoaderModule.PRELOADS_DIR)
val preloadsDir = File(filesDir, HookScriptLoaderModule.PRELOADS_DIR).apply { mkdirs() }
val out = File(preloadsDir, FILE_NAME) val out = File(preloadsDir, FILE_NAME)
if (out.exists()) {
// Defensive JS runtime patch. Avoid template literals with ${} to prevent Kotlin interpolation. out.delete()
val js = """ Log.i("PerfPatchesModule: deleted legacy perf_patches.js")
(function(){
try {
if (!globalThis.__SHIGGY_PERF_PATCHES__) {
globalThis.__SHIGGY_PERF_PATCHES__ = { installedAt: Date.now(), enabled: true };
}
function safe(fn) {
return function() {
try { return fn.apply(this, arguments); }
catch (e) { try { console && console.warn && console.warn('shiggy patch error', e); } catch(_){} }
};
}
function memoize(fn, maxEntries) {
if (typeof fn !== 'function') return fn;
var cache = new Map();
maxEntries = Number(maxEntries) || 128;
return function() {
var key;
try { key = JSON.stringify(Array.prototype.slice.call(arguments)); }
catch (e) { key = String(arguments.length) + ':' + String(arguments[0]); }
if (cache.has(key)) return cache.get(key);
var res = fn.apply(this, arguments);
cache.set(key, res);
if (cache.size > maxEntries) {
var it = cache.keys().next();
if (!it.done) cache.delete(it.value);
}
return res;
};
}
function createBatched(fn, intervalMs) {
if (typeof fn !== 'function') return fn;
var buf = [];
var timer = null;
intervalMs = Number(intervalMs) || 2000;
function flush() {
var batch = buf.splice(0, buf.length);
if (!batch.length) return;
try { fn.call(null, batch); }
catch (e) {
// Fallback: call per event asynchronously
try {
batch.forEach(function(ev){
setTimeout(function(){ try { fn(ev); } catch (_) {} }, 0);
});
} catch (_) {}
}
}
return function(event) {
try {
buf.push(event);
if (!timer) timer = setTimeout(function(){ timer = null; flush(); }, intervalMs);
} catch (e) {}
};
}
function patchIfExists(path, wrapper) {
try {
var parts = path.split('.');
var obj = globalThis;
for (var i = 0; i < parts.length - 1; i++) {
obj = obj && obj[parts[i]];
}
if (!obj) return false;
var key = parts[parts.length - 1];
if (typeof obj[key] !== 'function') return false;
obj[key] = wrapper(obj[key]);
return true;
} catch (e) { return false; }
}
// Try memoizing a few well-known functions
['computeHappeningNowState', 'computeNowState'].forEach(function(name){
try { patchIfExists(name, function(orig){ return memoize(orig, 128); }); } catch(_) {}
});
// Try caching RowGenerator.prototype.generate by instance signature
try {
var RG = globalThis && globalThis.RowGenerator;
if (RG && RG.prototype && typeof RG.prototype.generate === 'function') {
(function(){
var orig = RG.prototype.generate;
RG.prototype.generate = function() {
try {
var sig;
if (arguments && arguments.length) {
var a0 = arguments[0];
if (Array.isArray(a0)) {
sig = 'arr:' + a0.length + ':' + a0.slice(0,8).map(function(x){ return x && x.id ? x.id : ''; }).join(',');
} else if (a0 && a0.id) {
sig = 'id:' + a0.id;
} else {
try { sig = JSON.stringify(a0).slice(0,200); } catch(e){ sig = String(a0); }
}
} else sig = 'noargs';
if (this.__shiggy_sig === sig && typeof this.__shiggy_cache !== 'undefined') {
return this.__shiggy_cache;
}
var res = orig.apply(this, arguments);
this.__shiggy_sig = sig;
this.__shiggy_cache = res;
return res;
} catch (e) { return orig.apply(this, arguments); }
};
})();
}
} catch (e) {}
// Batch analytics-like methods (best-effort)
['app_ui_viewed', 'logEvent', 'Analytics.log', 'Analytics.track', 'analytics.trackEvent'].forEach(function(name){
try {
var parts = name.split('.');
var obj = globalThis;
for (var i = 0; i < parts.length - 1; i++) obj = obj && obj[parts[i]];
if (!obj) return;
var key = parts[parts.length - 1];
if (typeof obj[key] === 'function') obj[key] = createBatched(obj[key], 2000);
} catch (e) {}
});
// Make console methods async to avoid blocking hot-paths during startup
(function(){
try {
if (typeof console !== 'undefined') {
['log','info','warn','error','debug','trace'].forEach(function(m){
try {
if (typeof console[m] === 'function') {
var orig = console[m];
console[m] = function() {
var args = Array.prototype.slice.call(arguments);
setTimeout(function(){ try { orig.apply(console, args); } catch(_){} }, 0);
};
}
} catch(e){}
});
}
} catch(e){}
})();
// Deduplicate and slightly delay repeated kv calls to reduce IO contention on startup
(function(){
try {
var pending = new Map();
// Lightweight stats for dedupe wrapper to help telemetry/debugging in-case we need to tune delays.
try {
globalThis.__SHIGGY_PERF_DEDUPE_STATS__ = globalThis.__SHIGGY_PERF_DEDUPE_STATS__ || {
methods: {},
totalRequests: 0,
totalHits: 0,
totalMisses: 0,
pending: 0,
lastUpdated: Date.now()
};
} catch(e) {}
function _incStats(name, key) {
try {
var s = globalThis.__SHIGGY_PERF_DEDUPE_STATS__;
if (!s) return;
s.lastUpdated = Date.now();
var m = s.methods[name] || (s.methods[name] = { requests: 0, hits: 0, misses: 0, pending: 0 });
if (key === 'requests') { m.requests++; s.totalRequests++; }
else if (key === 'hits') { m.hits++; s.totalHits++; }
else if (key === 'misses') { m.misses++; s.totalMisses++; }
else if (key === 'pending_inc') { m.pending++; s.pending++; }
else if (key === 'pending_dec') { m.pending = Math.max(0, m.pending - 1); s.pending = Math.max(0, s.pending - 1); }
} catch(e) {}
}
function wrapAsyncDedupe(kvObj, methodName, delayMs){
try {
var orig = kvObj[methodName];
if (typeof orig !== 'function') return;
kvObj[methodName] = function() {
var argsArr = Array.prototype.slice.call(arguments);
var key;
try { key = JSON.stringify([methodName, argsArr]); } catch(e){ key = methodName + ':' + String(argsArr[0]); }
_incStats(methodName, 'requests');
if (pending.has(key)) { _incStats(methodName, 'hits'); return pending.get(key); }
_incStats(methodName, 'misses');
var self = this;
var p = new Promise(function(resolve, reject){
setTimeout(function(){
try {
var res = orig.apply(self, argsArr);
if (res && typeof res.then === 'function') res.then(resolve, reject);
else resolve(res);
} catch (e) { reject(e); }
}, Number(delayMs) || 20);
});
pending.set(key, p);
_incStats(methodName, 'pending_inc');
var cleanup = function(){ pending.delete(key); _incStats(methodName, 'pending_dec'); };
p.then(cleanup, cleanup);
return p;
};
} catch(e){}
}
if (globalThis.kv) {
if (globalThis.kv.get_many) wrapAsyncDedupe(globalThis.kv, 'get_many', 20);
if (globalThis.kv.get_kv_entries) wrapAsyncDedupe(globalThis.kv, 'get_kv_entries', 20);
}
} catch(e){}
})();
// Defer heavy filesystem/storage calls slightly to prioritize UI rendering
(function(){
try {
function makeDeferredIfAsync(obj, name, delayMs) {
try {
var orig = obj[name];
if (typeof orig !== 'function') return;
var isAsync = orig.constructor && orig.constructor.name === 'AsyncFunction';
var src = String(orig);
if (!isAsync && src.indexOf('return new Promise') === -1 && src.indexOf('.then(') === -1) {
// Likely synchronous - skip deferral to avoid changing semantics
return;
}
obj[name] = function() {
var args = Array.prototype.slice.call(arguments);
var self = this;
return new Promise(function(resolve, reject){
setTimeout(function(){
try {
var res = orig.apply(self, args);
if (res && typeof res.then === 'function') res.then(resolve, reject);
else resolve(res);
} catch (e) { reject(e); }
}, Number(delayMs) || 100);
});
};
} catch(e){}
}
if (globalThis.db && typeof globalThis.db.fs_info === 'function') {
makeDeferredIfAsync(globalThis.db, 'fs_info', 100);
}
if (globalThis.Storage && typeof globalThis.Storage.refresh === 'function') {
makeDeferredIfAsync(globalThis.Storage, 'refresh', 50);
}
} catch(e){}
})();
// Expose lightweight API
try {
if (!globalThis.__SHIGGY_PERF_API__) {
globalThis.__SHIGGY_PERF_API__ = {
installedAt: Date.now(),
enabled: true,
uninstall: function() { try { globalThis.__SHIGGY_PERF_API__.enabled = false; } catch(e) {} }
};
}
} catch (e) {}
} catch (e) {
try { console && console.warn && console.warn('PerfPatches: error', e); } catch(_) {}
}
})();
""".trimIndent()
// Write file if absent or changed
try {
if (!out.exists() || out.readText() != js) {
out.writeText(js)
Log.i("PerfPatchesModule: wrote patch to " + out.absolutePath)
} else {
Log.i("PerfPatchesModule: perf patch already up-to-date")
}
} catch (e: Throwable) {
Log.e("PerfPatchesModule: failed writing patch", e)
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Log.e("PerfPatchesModule:onLoad failed", e) Log.e("PerfPatchesModule: cleanup failed", e)
} }
} }
@Deprecated("Overrides deprecated Module.buildPayload") @Deprecated("Overrides deprecated Module.buildPayload")
override fun buildPayload(builder: JsonObjectBuilder) { override fun buildPayload(builder: JsonObjectBuilder) {
builder.put("perfPatches", true) builder.put("perfPatches", false)
} }
} }

View file

@ -76,7 +76,7 @@ object FontsModule : Module() {
classLoader, classLoader,
"createAssetTypeface", "createAssetTypeface",
String::class.java, String::class.java,
Int::class.java, Int::class.javaPrimitiveType ?: java.lang.Integer.TYPE,
"android.content.res.AssetManager", "android.content.res.AssetManager",
replacement replacement
) )