[core] Refine caching of config manager (#878)

This commit is contained in:
LoveSy 2021-08-10 04:18:22 +08:00 committed by GitHub
parent 3cd9fd1735
commit a84935b14e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 126 additions and 101 deletions

View File

@ -3,6 +3,7 @@ import org.lsposed.lspd.models.PreLoadedApk;
parcelable Module { parcelable Module {
String packageName; String packageName;
int appId;
String apkPath; String apkPath;
PreLoadedApk file; PreLoadedApk file;
} }

View File

@ -27,7 +27,9 @@ import android.app.ActivityThread;
import android.app.IApplicationThread; import android.app.IApplicationThread;
import android.content.Context; import android.content.Context;
import android.os.Binder; import android.os.Binder;
import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
import android.os.Looper;
import android.os.Parcel; import android.os.Parcel;
import android.os.Process; import android.os.Process;
import android.os.RemoteException; import android.os.RemoteException;
@ -94,7 +96,7 @@ public class BridgeService {
bridgeService.unlinkToDeath(this, 0); bridgeService.unlinkToDeath(this, 0);
bridgeService = null; bridgeService = null;
listener.onSystemServerDied(); listener.onSystemServerDied();
new Thread(() -> sendToBridge(serviceBinder, true)).start(); new Handler(Looper.getMainLooper()).post(() -> sendToBridge(serviceBinder, true));
} }
}; };

View File

@ -19,6 +19,8 @@
package org.lsposed.lspd.service; 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 static org.lsposed.lspd.service.ServiceManager.TAG;
import android.content.ContentValues; 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. // This config manager assume uid won't change when our service is off.
// Otherwise, user should maintain it manually. // Otherwise, user should maintain it manually.
public class ConfigManager { public class ConfigManager {
public static final int PER_USER_RANGE = 100000;
private static final String[] MANAGER_PERMISSIONS_TO_GRANT = new String[]{ private static final String[] MANAGER_PERMISSIONS_TO_GRANT = new String[]{
"android.permission.INTERACT_ACROSS_USERS", "android.permission.INTERACT_ACROSS_USERS",
@ -202,11 +203,8 @@ public class ConfigManager {
private final Map<ProcessScope, List<Module>> cachedScope = new ConcurrentHashMap<>(); private final Map<ProcessScope, List<Module>> cachedScope = new ConcurrentHashMap<>();
// apkPath, PreLoadedApk // packageName, Module
private final Map<String, PreLoadedApk> cachedApkFile = new ConcurrentHashMap<>(); private final Map<String, Module> cachedModule = new ConcurrentHashMap<>();
// appId, packageName
private final Map<Integer, String> cachedModule = new ConcurrentHashMap<>();
// packageName, userId, group, key, value // packageName, userId, group, key, value
private final Map<Pair<String, Integer>, Map<String, ConcurrentHashMap<String, Object>>> cachedConfig = new ConcurrentHashMap<>(); private final Map<Pair<String, Integer>, Map<String, ConcurrentHashMap<String, Object>>> cachedConfig = new ConcurrentHashMap<>();
@ -217,10 +215,8 @@ public class ConfigManager {
} }
if (sync) { if (sync) {
cacheModules(); cacheModules();
cacheScopes();
} else { } else {
cacheHandler.post(this::cacheModules); cacheHandler.post(this::cacheModules);
cacheHandler.post(this::cacheScopes);
} }
} }
@ -259,16 +255,21 @@ public class ConfigManager {
int pkgNameIdx = cursor.getColumnIndex("module_pkg_name"); int pkgNameIdx = cursor.getColumnIndex("module_pkg_name");
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
var path = cursor.getString(apkPathIdx); var path = cursor.getString(apkPathIdx);
var file = getCachedApkFile(path); var packageName = cursor.getString(pkgNameIdx);
if (file != null) { var m = cachedModule.computeIfAbsent(packageName, p -> {
var module = new Module(); 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.packageName = cursor.getString(pkgNameIdx);
module.apkPath = path; module.apkPath = path;
module.file = file; module.file = file;
modules.add(module); module.appId = -1;
} else { return module;
Log.w(TAG, "Can not load " + path + ", skip!"); });
} if (m != null) modules.add(m);
} }
} }
return modules; return modules;
@ -438,55 +439,71 @@ public class ConfigManager {
if (!packageStarted) return; if (!packageStarted) return;
if (lastModuleCacheTime >= requestModuleCacheTime) return; if (lastModuleCacheTime >= requestModuleCacheTime) return;
else lastModuleCacheTime = SystemClock.elapsedRealtime(); else lastModuleCacheTime = SystemClock.elapsedRealtime();
cachedModule.clear(); try (Cursor cursor = db.query(true, "modules", new String[]{"module_pkg_name", "apk_path"},
try (Cursor cursor = db.query(true, "modules INNER JOIN scope ON scope.mid = modules.mid", new String[]{"module_pkg_name", "user_id"},
"enabled = 1", null, null, null, null, null)) { "enabled = 1", null, null, null, null, null)) {
if (cursor == null) { if (cursor == null) {
Log.e(TAG, "db cache failed"); Log.e(TAG, "db cache failed");
return; return;
} }
int pkgNameIdx = cursor.getColumnIndex("module_pkg_name"); int pkgNameIdx = cursor.getColumnIndex("module_pkg_name");
int userIdIdx = cursor.getColumnIndex("user_id"); int apkPathIdx = cursor.getColumnIndex("apk_path");
// packageName, userId, packageInfo
Map<String, Map<Integer, PackageInfo>> modules = new HashMap<>();
Set<String> obsoleteModules = new HashSet<>(); Set<String> obsoleteModules = new HashSet<>();
Set<Application> obsoleteScopes = new HashSet<>(); // packageName, apkPath
Map<String, String> obsoletePaths = new HashMap<>();
cachedModule.values().removeIf(m -> m.apkPath == null || !new File(m.apkPath).exists());
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
String packageName = cursor.getString(pkgNameIdx); String packageName = cursor.getString(pkgNameIdx);
int userId = cursor.getInt(userIdIdx); String apkPath = cursor.getString(apkPathIdx);
var pkgInfo = modules.computeIfAbsent(packageName, m -> { // 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 { try {
return PackageService.getPackageInfoFromAllUsers(m, 0); pkgInfo = PackageService.getPackageInfo(packageName, MATCH_ALL_FLAGS, 0);
} catch (Throwable e) { } catch (Throwable e) {
Log.e(TAG, Log.getStackTraceString(e)); Log.w(TAG, "get package info of " + packageName, e);
return Collections.emptyMap();
} }
}); if (pkgInfo == null || pkgInfo.applicationInfo == null) {
if (pkgInfo.isEmpty()) {
obsoleteModules.add(packageName); obsoleteModules.add(packageName);
} else if (!pkgInfo.containsKey(userId)) { continue;
var module = new Application(); }
// 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.packageName = packageName;
module.userId = userId; module.file = file;
obsoleteScopes.add(module); module.appId = pkgInfo.applicationInfo.uid;
} else { cachedModule.put(packageName, module);
var info = pkgInfo.get(userId);
assert info != null;
cachedModule.computeIfAbsent(info.applicationInfo.uid % PER_USER_RANGE, k -> info.packageName);
} }
} obsoleteModules.forEach(this::removeModuleWithoutCache);
for (var obsoleteModule : obsoleteModules) { obsoletePaths.forEach(this::updateModuleApkPath);
removeModuleWithoutCache(obsoleteModule);
}
for (var obsoleteScope : obsoleteScopes) {
removeModuleScopeWithoutCache(obsoleteScope);
}
checkCachedApkFile();
} }
Log.d(TAG, "cached modules"); Log.d(TAG, "cached modules");
for (int uid : cachedModule.keySet()) { for (String module : cachedModule.keySet()) {
Log.d(TAG, cachedModule.get(uid) + "/" + uid); Log.d(TAG, module);
} }
cacheScopes();
} }
private synchronized void cacheScopes() { private synchronized void cacheScopes() {
@ -495,42 +512,48 @@ public class ConfigManager {
if (lastScopeCacheTime >= requestScopeCacheTime) return; if (lastScopeCacheTime >= requestScopeCacheTime) return;
else lastScopeCacheTime = SystemClock.elapsedRealtime(); else lastScopeCacheTime = SystemClock.elapsedRealtime();
cachedScope.clear(); 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)) { "enabled = 1", null, null, null, null)) {
if (cursor == null) {
Log.e(TAG, "db cache failed");
return;
}
int appPkgNameIdx = cursor.getColumnIndex("app_pkg_name"); int appPkgNameIdx = cursor.getColumnIndex("app_pkg_name");
int modulePkgNameIdx = cursor.getColumnIndex("module_pkg_name"); int modulePkgNameIdx = cursor.getColumnIndex("module_pkg_name");
int userIdIdx = cursor.getColumnIndex("user_id"); int userIdIdx = cursor.getColumnIndex("user_id");
int apkPathIdx = cursor.getColumnIndex("apk_path");
HashSet<Application> obsoletePackages = new HashSet<>(); final var obsoletePackages = new HashSet<Application>();
final var obsoleteModules = new HashSet<Application>();
final var moduleAvailability = new HashMap<Pair<String, Integer>, Boolean>();
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
Application app = new Application(); Application app = new Application();
app.packageName = cursor.getString(appPkgNameIdx); app.packageName = cursor.getString(appPkgNameIdx);
app.userId = cursor.getInt(userIdIdx); 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 // system server always loads database
if (app.packageName.equals("android")) continue; if (app.packageName.equals("android")) continue;
try { try {
List<ProcessScope> processesScope = getAssociatedProcesses(app); List<ProcessScope> processesScope = getAssociatedProcesses(app);
if (processesScope.isEmpty()) { if (processesScope.isEmpty()) {
obsoletePackages.add(app); obsoletePackages.add(app);
continue; continue;
} }
var apkPath = cursor.getString(apkPathIdx); var module = cachedModule.get(modulePackageName);
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;
}
for (ProcessScope processScope : processesScope) { for (ProcessScope processScope : processesScope) {
cachedScope.computeIfAbsent(processScope, cachedScope.computeIfAbsent(processScope,
ignored -> new LinkedList<>()).add(module); ignored -> new LinkedList<>()).add(module);
@ -553,6 +576,10 @@ public class ConfigManager {
Log.d(TAG, "removing obsolete package: " + obsoletePackage.packageName + "/" + obsoletePackage.userId); Log.d(TAG, "removing obsolete package: " + obsoletePackage.packageName + "/" + obsoletePackage.userId);
removeAppWithoutCache(obsoletePackage); removeAppWithoutCache(obsoletePackage);
} }
for (Application obsoleteModule : obsoleteModules) {
Log.d(TAG, "removing obsolete module: " + obsoleteModule.packageName + "/" + obsoleteModule.userId);
removeModuleScopeWithoutCache(obsoleteModule);
}
} }
Log.d(TAG, "cached Scope"); Log.d(TAG, "cached Scope");
cachedScope.forEach((ps, modules) -> { cachedScope.forEach((ps, modules) -> {
@ -616,23 +643,6 @@ public class ConfigManager {
return file; 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 // This is called when a new process created, use the cached result
public List<Module> getModulesForProcess(String processName, int uid) { public List<Module> getModulesForProcess(String processName, int uid) {
return isManager(uid) ? Collections.emptyList() : cachedScope.getOrDefault(new ProcessScope(processName, uid), Collections.emptyList()); 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; String[] apks;
if (info.splitSourceDirs != null) { if (info.splitSourceDirs != null) {
apks = Arrays.copyOf(info.splitSourceDirs, info.splitSourceDirs.length + 1); apks = Arrays.copyOf(info.splitSourceDirs, info.splitSourceDirs.length + 1);
@ -682,14 +692,18 @@ public class ConfigManager {
return false; return false;
} }
}).findFirst(); }).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()) { if (db.inTransaction()) {
Log.w(TAG, "update module apk path should not be called inside transaction"); Log.w(TAG, "update module apk path should not be called inside transaction");
return false; return false;
} }
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put("module_pkg_name", packageName); 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); int count = (int) db.insertWithOnConflict("modules", null, values, SQLiteDatabase.CONFLICT_IGNORE);
if (count < 0) { if (count < 0) {
count = db.updateWithOnConflict("modules", values, "module_pkg_name=?", new String[]{packageName}, SQLiteDatabase.CONFLICT_IGNORE); 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) { public boolean enableModule(String packageName, ApplicationInfo info) {
if (!updateModuleApkPath(packageName, info)) return false; if (!updateModuleApkPath(packageName, getModuleApkPath(info))) return false;
int mid = getModuleId(packageName); int mid = getModuleId(packageName);
if (mid == -1) return false; if (mid == -1) return false;
try { try {
@ -922,12 +936,17 @@ public class ConfigManager {
}); });
} }
// this is slow, avoid using it
public boolean isModule(int uid) { 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) { 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 { private void recursivelyChown(File file, int uid, int gid) throws ErrnoException {
@ -939,8 +958,7 @@ public class ConfigManager {
} }
} }
public boolean ensureModulePrefsPermission(int uid) { public boolean ensureModulePrefsPermission(int uid, String packageName) {
String packageName = cachedModule.get(uid);
if (packageName == null) return false; if (packageName == null) return false;
File path = new File(getPrefsPath(packageName, uid)); File path = new File(getPrefsPath(packageName, uid));
try { try {

View File

@ -104,7 +104,7 @@ public class LSPApplicationService extends ILSPApplicationService.Stub {
public IBinder requestModuleBinder(String name) throws RemoteException { public IBinder requestModuleBinder(String name) throws RemoteException {
ensureRegistered(); ensureRegistered();
if (ConfigManager.getInstance().isModule(getCallingUid(), name)) { if (ConfigManager.getInstance().isModule(getCallingUid(), name)) {
ConfigManager.getInstance().ensureModulePrefsPermission(getCallingUid()); ConfigManager.getInstance().ensureModulePrefsPermission(getCallingUid(), name);
return ServiceManager.getModuleService(name); return ServiceManager.getModuleService(name);
} else return null; } else return null;
} }

View File

@ -19,7 +19,7 @@
package org.lsposed.lspd.service; 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 static org.lsposed.lspd.service.ServiceManager.TAG;
import android.app.IApplicationThread; 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) // when package is changed, we may need to update cache (module cache or process cache)
if (isXposedModule) { if (isXposedModule) {
var ret = ConfigManager.getInstance().updateModuleApkPath(moduleName, applicationInfo); ConfigManager.getInstance().updateCache();
if (ret) Log.i(TAG, "Updated module apk path: " + moduleName);
} else if (ConfigManager.getInstance().isUidHooked(uid)) { } else if (ConfigManager.getInstance().isUidHooked(uid)) {
// it will automatically remove obsolete app from database // it will automatically remove obsolete app from database
ConfigManager.getInstance().updateAppCache(); ConfigManager.getInstance().updateAppCache();

View File

@ -74,6 +74,7 @@ public class PackageService {
static final int INSTALL_REASON_UNKNOWN = 0; 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; 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 IPackageManager pm = null;
private static IBinder binder = null; private static IBinder binder = null;
@ -135,7 +136,7 @@ public class PackageService {
if (filterNoProcess) { if (filterNoProcess) {
res.removeIf(packageInfo -> { res.removeIf(packageInfo -> {
try { 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(); return fetchProcesses(pkgInfo).isEmpty();
} catch (RemoteException e) { } catch (RemoteException e) {
return false; return false;
@ -178,6 +179,10 @@ public class PackageService {
return new Pair<>(fetchProcesses(pkgInfo), pkgInfo.applicationInfo.uid); 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 { private static PackageInfo getPackageInfoWithComponents(String packageName, int flags, int userId) throws RemoteException {
IPackageManager pm = getPackageManager(); IPackageManager pm = getPackageManager();
if (pm == null) return null; 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 null;
return pkgInfo; return pkgInfo;
} }