From a84935b14e5eb6f4a794feb635ba7ee1f90571f5 Mon Sep 17 00:00:00 2001 From: LoveSy Date: Tue, 10 Aug 2021 04:18:22 +0800 Subject: [PATCH] [core] Refine caching of config manager (#878) --- .../aidl/org/lsposed/lspd/models/Module.aidl | 1 + .../lsposed/lspd/service/BridgeService.java | 4 +- .../lsposed/lspd/service/ConfigManager.java | 206 ++++++++++-------- .../lspd/service/LSPApplicationService.java | 2 +- .../lsposed/lspd/service/LSPosedService.java | 5 +- .../lsposed/lspd/service/PackageService.java | 9 +- 6 files changed, 126 insertions(+), 101 deletions(-) diff --git a/core/src/main/aidl/org/lsposed/lspd/models/Module.aidl b/core/src/main/aidl/org/lsposed/lspd/models/Module.aidl index 4a494ffb..f98df183 100644 --- a/core/src/main/aidl/org/lsposed/lspd/models/Module.aidl +++ b/core/src/main/aidl/org/lsposed/lspd/models/Module.aidl @@ -3,6 +3,7 @@ import org.lsposed.lspd.models.PreLoadedApk; parcelable Module { String packageName; + int appId; String apkPath; PreLoadedApk file; } diff --git a/core/src/main/java/org/lsposed/lspd/service/BridgeService.java b/core/src/main/java/org/lsposed/lspd/service/BridgeService.java index d861458c..a097c516 100644 --- a/core/src/main/java/org/lsposed/lspd/service/BridgeService.java +++ b/core/src/main/java/org/lsposed/lspd/service/BridgeService.java @@ -27,7 +27,9 @@ import android.app.ActivityThread; import android.app.IApplicationThread; import android.content.Context; import android.os.Binder; +import android.os.Handler; import android.os.IBinder; +import android.os.Looper; import android.os.Parcel; import android.os.Process; import android.os.RemoteException; @@ -94,7 +96,7 @@ public class BridgeService { bridgeService.unlinkToDeath(this, 0); bridgeService = null; listener.onSystemServerDied(); - new Thread(() -> sendToBridge(serviceBinder, true)).start(); + new Handler(Looper.getMainLooper()).post(() -> sendToBridge(serviceBinder, true)); } }; diff --git a/core/src/main/java/org/lsposed/lspd/service/ConfigManager.java b/core/src/main/java/org/lsposed/lspd/service/ConfigManager.java index 2827a6ce..585cb449 100644 --- a/core/src/main/java/org/lsposed/lspd/service/ConfigManager.java +++ b/core/src/main/java/org/lsposed/lspd/service/ConfigManager.java @@ -19,6 +19,8 @@ package org.lsposed.lspd.service; +import static org.lsposed.lspd.service.PackageService.MATCH_ALL_FLAGS; +import static org.lsposed.lspd.service.PackageService.PER_USER_RANGE; import static org.lsposed.lspd.service.ServiceManager.TAG; import android.content.ContentValues; @@ -79,7 +81,6 @@ import java.util.zip.ZipFile; // This config manager assume uid won't change when our service is off. // Otherwise, user should maintain it manually. public class ConfigManager { - public static final int PER_USER_RANGE = 100000; private static final String[] MANAGER_PERMISSIONS_TO_GRANT = new String[]{ "android.permission.INTERACT_ACROSS_USERS", @@ -202,11 +203,8 @@ public class ConfigManager { private final Map> cachedScope = new ConcurrentHashMap<>(); - // apkPath, PreLoadedApk - private final Map cachedApkFile = new ConcurrentHashMap<>(); - - // appId, packageName - private final Map cachedModule = new ConcurrentHashMap<>(); + // packageName, Module + private final Map cachedModule = new ConcurrentHashMap<>(); // packageName, userId, group, key, value private final Map, Map>> cachedConfig = new ConcurrentHashMap<>(); @@ -217,10 +215,8 @@ public class ConfigManager { } if (sync) { cacheModules(); - cacheScopes(); } else { cacheHandler.post(this::cacheModules); - cacheHandler.post(this::cacheScopes); } } @@ -259,16 +255,21 @@ public class ConfigManager { int pkgNameIdx = cursor.getColumnIndex("module_pkg_name"); while (cursor.moveToNext()) { var path = cursor.getString(apkPathIdx); - var file = getCachedApkFile(path); - if (file != null) { + var packageName = cursor.getString(pkgNameIdx); + var m = cachedModule.computeIfAbsent(packageName, p -> { var module = new Module(); + var file = loadModule(path); + if (file == null) { + Log.w(TAG, "Can not load " + path + ", skip!"); + return null; + } module.packageName = cursor.getString(pkgNameIdx); module.apkPath = path; module.file = file; - modules.add(module); - } else { - Log.w(TAG, "Can not load " + path + ", skip!"); - } + module.appId = -1; + return module; + }); + if (m != null) modules.add(m); } } return modules; @@ -438,55 +439,71 @@ public class ConfigManager { if (!packageStarted) return; if (lastModuleCacheTime >= requestModuleCacheTime) return; else lastModuleCacheTime = SystemClock.elapsedRealtime(); - cachedModule.clear(); - try (Cursor cursor = db.query(true, "modules INNER JOIN scope ON scope.mid = modules.mid", new String[]{"module_pkg_name", "user_id"}, + try (Cursor cursor = db.query(true, "modules", new String[]{"module_pkg_name", "apk_path"}, "enabled = 1", null, null, null, null, null)) { if (cursor == null) { Log.e(TAG, "db cache failed"); return; } int pkgNameIdx = cursor.getColumnIndex("module_pkg_name"); - int userIdIdx = cursor.getColumnIndex("user_id"); - // packageName, userId, packageInfo - Map> modules = new HashMap<>(); + int apkPathIdx = cursor.getColumnIndex("apk_path"); Set obsoleteModules = new HashSet<>(); - Set obsoleteScopes = new HashSet<>(); + // packageName, apkPath + Map obsoletePaths = new HashMap<>(); + cachedModule.values().removeIf(m -> m.apkPath == null || !new File(m.apkPath).exists()); while (cursor.moveToNext()) { String packageName = cursor.getString(pkgNameIdx); - int userId = cursor.getInt(userIdIdx); - var pkgInfo = modules.computeIfAbsent(packageName, m -> { - try { - return PackageService.getPackageInfoFromAllUsers(m, 0); - } catch (Throwable e) { - Log.e(TAG, Log.getStackTraceString(e)); - return Collections.emptyMap(); - } - }); - if (pkgInfo.isEmpty()) { - obsoleteModules.add(packageName); - } else if (!pkgInfo.containsKey(userId)) { - var module = new Application(); - module.packageName = packageName; - module.userId = userId; - obsoleteScopes.add(module); - } else { - var info = pkgInfo.get(userId); - assert info != null; - cachedModule.computeIfAbsent(info.applicationInfo.uid % PER_USER_RANGE, k -> info.packageName); + String apkPath = cursor.getString(apkPathIdx); + // if still present after removeIf, this package did not change. + var oldModule = cachedModule.get(packageName); + if (oldModule != null && oldModule.appId != -1) { + Log.d(TAG, packageName + " did not change, skip caching it"); + continue; } + PackageInfo pkgInfo = null; + try { + pkgInfo = PackageService.getPackageInfo(packageName, MATCH_ALL_FLAGS, 0); + } catch (Throwable e) { + Log.w(TAG, "get package info of " + packageName, e); + } + if (pkgInfo == null || pkgInfo.applicationInfo == null) { + obsoleteModules.add(packageName); + continue; + } + // cache from system server, keep it and set only the appId + if (oldModule != null) { + oldModule.appId = pkgInfo.applicationInfo.uid; + continue; + } + var path = apkPath; + if (!new File(path).exists()) { + path = getModuleApkPath(pkgInfo.applicationInfo); + if (path == null) + obsoleteModules.add(packageName); + else + obsoletePaths.put(packageName, path); + } + var file = loadModule(path); + if (file == null) { + Log.w(TAG, "failed to load module " + packageName); + obsoleteModules.add(packageName); + continue; + } + var module = new Module(); + module.apkPath = path; + module.packageName = packageName; + module.file = file; + module.appId = pkgInfo.applicationInfo.uid; + cachedModule.put(packageName, module); } - for (var obsoleteModule : obsoleteModules) { - removeModuleWithoutCache(obsoleteModule); - } - for (var obsoleteScope : obsoleteScopes) { - removeModuleScopeWithoutCache(obsoleteScope); - } - checkCachedApkFile(); + obsoleteModules.forEach(this::removeModuleWithoutCache); + obsoletePaths.forEach(this::updateModuleApkPath); } Log.d(TAG, "cached modules"); - for (int uid : cachedModule.keySet()) { - Log.d(TAG, cachedModule.get(uid) + "/" + uid); + for (String module : cachedModule.keySet()) { + Log.d(TAG, module); } + cacheScopes(); } private synchronized void cacheScopes() { @@ -495,42 +512,48 @@ public class ConfigManager { if (lastScopeCacheTime >= requestScopeCacheTime) return; else lastScopeCacheTime = SystemClock.elapsedRealtime(); cachedScope.clear(); - try (Cursor cursor = db.query("scope INNER JOIN modules ON scope.mid = modules.mid", new String[]{"app_pkg_name", "module_pkg_name", "user_id", "apk_path"}, + try (Cursor cursor = db.query("scope INNER JOIN modules ON scope.mid = modules.mid", new String[]{"app_pkg_name", "module_pkg_name", "user_id"}, "enabled = 1", null, null, null, null)) { - if (cursor == null) { - Log.e(TAG, "db cache failed"); - return; - } int appPkgNameIdx = cursor.getColumnIndex("app_pkg_name"); int modulePkgNameIdx = cursor.getColumnIndex("module_pkg_name"); int userIdIdx = cursor.getColumnIndex("user_id"); - int apkPathIdx = cursor.getColumnIndex("apk_path"); - HashSet obsoletePackages = new HashSet<>(); + + final var obsoletePackages = new HashSet(); + final var obsoleteModules = new HashSet(); + final var moduleAvailability = new HashMap, Boolean>(); while (cursor.moveToNext()) { Application app = new Application(); app.packageName = cursor.getString(appPkgNameIdx); app.userId = cursor.getInt(userIdIdx); + var modulePackageName = cursor.getString(modulePkgNameIdx); + + // check if module is present in this user + if (!moduleAvailability.computeIfAbsent(new Pair<>(modulePackageName, app.userId), n -> { + var available = false; + try { + available = PackageService.isPackageAvailable(n.first, n.second, true) && cachedModule.containsKey(modulePackageName); + } catch (Throwable e) { + Log.w(TAG, "check package availability ", e); + } + if (!available) { + var obsoleteModule = new Application(); + obsoleteModule.packageName = modulePackageName; + obsoleteModule.userId = app.userId; + obsoleteModules.add(obsoleteModule); + } + return available; + })) continue; + // system server always loads database if (app.packageName.equals("android")) continue; + try { List processesScope = getAssociatedProcesses(app); if (processesScope.isEmpty()) { obsoletePackages.add(app); continue; } - var apkPath = cursor.getString(apkPathIdx); - var modulePackageName = cursor.getString(modulePkgNameIdx); - var file = getCachedApkFile(apkPath); - Module module; - if (file != null) { - module = new Module(); - module.packageName = modulePackageName; - module.apkPath = apkPath; - module.file = file; - } else { - Log.w(TAG, "Can not load " + apkPath + ", skip!"); - continue; - } + var module = cachedModule.get(modulePackageName); for (ProcessScope processScope : processesScope) { cachedScope.computeIfAbsent(processScope, ignored -> new LinkedList<>()).add(module); @@ -553,6 +576,10 @@ public class ConfigManager { Log.d(TAG, "removing obsolete package: " + obsoletePackage.packageName + "/" + obsoletePackage.userId); removeAppWithoutCache(obsoletePackage); } + for (Application obsoleteModule : obsoleteModules) { + Log.d(TAG, "removing obsolete module: " + obsoleteModule.packageName + "/" + obsoleteModule.userId); + removeModuleScopeWithoutCache(obsoleteModule); + } } Log.d(TAG, "cached Scope"); cachedScope.forEach((ps, modules) -> { @@ -616,23 +643,6 @@ public class ConfigManager { return file; } - @Nullable - private PreLoadedApk getCachedApkFile(String path) { - return cachedApkFile.computeIfAbsent(path, this::loadModule); - } - - private void checkCachedApkFile() { - cachedApkFile.entrySet().removeIf(entry -> { - var path = entry.getKey(); - var file = entry.getValue(); - if (!new File(path).exists()) { - file.preLoadedDexes.stream().parallel().forEach(SharedMemory::close); - return true; - } - return false; - }); - } - // This is called when a new process created, use the cached result public List getModulesForProcess(String processName, int uid) { return isManager(uid) ? Collections.emptyList() : cachedScope.getOrDefault(new ProcessScope(processName, uid), Collections.emptyList()); @@ -669,7 +679,7 @@ public class ConfigManager { } } - public boolean updateModuleApkPath(String packageName, ApplicationInfo info) { + public String getModuleApkPath(ApplicationInfo info) { String[] apks; if (info.splitSourceDirs != null) { apks = Arrays.copyOf(info.splitSourceDirs, info.splitSourceDirs.length + 1); @@ -682,14 +692,18 @@ public class ConfigManager { return false; } }).findFirst(); - if (!apkPath.isPresent()) return false; + return apkPath.orElse(null); + } + + public boolean updateModuleApkPath(String packageName, String apkPath) { + if (apkPath == null) return false; if (db.inTransaction()) { Log.w(TAG, "update module apk path should not be called inside transaction"); return false; } ContentValues values = new ContentValues(); values.put("module_pkg_name", packageName); - values.put("apk_path", apkPath.get()); + values.put("apk_path", apkPath); int count = (int) db.insertWithOnConflict("modules", null, values, SQLiteDatabase.CONFLICT_IGNORE); if (count < 0) { count = db.updateWithOnConflict("modules", values, "module_pkg_name=?", new String[]{packageName}, SQLiteDatabase.CONFLICT_IGNORE); @@ -811,7 +825,7 @@ public class ConfigManager { } public boolean enableModule(String packageName, ApplicationInfo info) { - if (!updateModuleApkPath(packageName, info)) return false; + if (!updateModuleApkPath(packageName, getModuleApkPath(info))) return false; int mid = getModuleId(packageName); if (mid == -1) return false; try { @@ -922,12 +936,17 @@ public class ConfigManager { }); } + // this is slow, avoid using it public boolean isModule(int uid) { - return cachedModule.containsKey(uid % PER_USER_RANGE); + for (var module : cachedModule.values()) { + if (module.appId == uid % PER_USER_RANGE) return true; + } + return false; } public boolean isModule(int uid, String name) { - return name.equals(cachedModule.getOrDefault(uid % PER_USER_RANGE, null)); + var module = cachedModule.getOrDefault(name, null); + return module != null && module.appId == uid % PER_USER_RANGE; } private void recursivelyChown(File file, int uid, int gid) throws ErrnoException { @@ -939,8 +958,7 @@ public class ConfigManager { } } - public boolean ensureModulePrefsPermission(int uid) { - String packageName = cachedModule.get(uid); + public boolean ensureModulePrefsPermission(int uid, String packageName) { if (packageName == null) return false; File path = new File(getPrefsPath(packageName, uid)); try { diff --git a/core/src/main/java/org/lsposed/lspd/service/LSPApplicationService.java b/core/src/main/java/org/lsposed/lspd/service/LSPApplicationService.java index f7adb791..259ea929 100644 --- a/core/src/main/java/org/lsposed/lspd/service/LSPApplicationService.java +++ b/core/src/main/java/org/lsposed/lspd/service/LSPApplicationService.java @@ -104,7 +104,7 @@ public class LSPApplicationService extends ILSPApplicationService.Stub { public IBinder requestModuleBinder(String name) throws RemoteException { ensureRegistered(); if (ConfigManager.getInstance().isModule(getCallingUid(), name)) { - ConfigManager.getInstance().ensureModulePrefsPermission(getCallingUid()); + ConfigManager.getInstance().ensureModulePrefsPermission(getCallingUid(), name); return ServiceManager.getModuleService(name); } else return null; } diff --git a/core/src/main/java/org/lsposed/lspd/service/LSPosedService.java b/core/src/main/java/org/lsposed/lspd/service/LSPosedService.java index d89ebb42..6c107631 100644 --- a/core/src/main/java/org/lsposed/lspd/service/LSPosedService.java +++ b/core/src/main/java/org/lsposed/lspd/service/LSPosedService.java @@ -19,7 +19,7 @@ package org.lsposed.lspd.service; -import static org.lsposed.lspd.service.ConfigManager.PER_USER_RANGE; +import static org.lsposed.lspd.service.PackageService.PER_USER_RANGE; import static org.lsposed.lspd.service.ServiceManager.TAG; import android.app.IApplicationThread; @@ -113,8 +113,7 @@ public class LSPosedService extends ILSPosedService.Stub { } // when package is changed, we may need to update cache (module cache or process cache) if (isXposedModule) { - var ret = ConfigManager.getInstance().updateModuleApkPath(moduleName, applicationInfo); - if (ret) Log.i(TAG, "Updated module apk path: " + moduleName); + ConfigManager.getInstance().updateCache(); } else if (ConfigManager.getInstance().isUidHooked(uid)) { // it will automatically remove obsolete app from database ConfigManager.getInstance().updateAppCache(); diff --git a/core/src/main/java/org/lsposed/lspd/service/PackageService.java b/core/src/main/java/org/lsposed/lspd/service/PackageService.java index 33d5f56f..34f0be84 100644 --- a/core/src/main/java/org/lsposed/lspd/service/PackageService.java +++ b/core/src/main/java/org/lsposed/lspd/service/PackageService.java @@ -74,6 +74,7 @@ public class PackageService { static final int INSTALL_REASON_UNKNOWN = 0; static final int MATCH_ALL_FLAGS = PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.MATCH_UNINSTALLED_PACKAGES; + public static final int PER_USER_RANGE = 100000; private static IPackageManager pm = null; private static IBinder binder = null; @@ -135,7 +136,7 @@ public class PackageService { if (filterNoProcess) { res.removeIf(packageInfo -> { try { - PackageInfo pkgInfo = getPackageInfoWithComponents(packageInfo.packageName, MATCH_ALL_FLAGS, packageInfo.applicationInfo.uid / 100000); + PackageInfo pkgInfo = getPackageInfoWithComponents(packageInfo.packageName, MATCH_ALL_FLAGS, packageInfo.applicationInfo.uid / PER_USER_RANGE); return fetchProcesses(pkgInfo).isEmpty(); } catch (RemoteException e) { return false; @@ -178,6 +179,10 @@ public class PackageService { return new Pair<>(fetchProcesses(pkgInfo), pkgInfo.applicationInfo.uid); } + public static boolean isPackageAvailable(String packageName, int userId, boolean ignoreHidden) throws RemoteException { + return pm.isPackageAvailable(packageName, userId) && (!ignoreHidden || pm.getApplicationHiddenSettingAsUser(packageName, userId)); + } + private static PackageInfo getPackageInfoWithComponents(String packageName, int flags, int userId) throws RemoteException { IPackageManager pm = getPackageManager(); if (pm == null) return null; @@ -208,7 +213,7 @@ public class PackageService { } } - if (pkgInfo == null || pkgInfo.applicationInfo == null || (!pkgInfo.packageName.equals("android") && (pkgInfo.applicationInfo.sourceDir == null || !new File(pkgInfo.applicationInfo.sourceDir).exists() || (!pm.isPackageAvailable(packageName, userId) && !pm.getApplicationHiddenSettingAsUser(packageName, userId))))) + if (pkgInfo == null || pkgInfo.applicationInfo == null || (!pkgInfo.packageName.equals("android") && (pkgInfo.applicationInfo.sourceDir == null || !new File(pkgInfo.applicationInfo.sourceDir).exists() || isPackageAvailable(packageName, userId, true)))) return null; return pkgInfo; }