[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 {
String packageName;
int appId;
String apkPath;
PreLoadedApk file;
}

View File

@ -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));
}
};

View File

@ -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<ProcessScope, List<Module>> cachedScope = new ConcurrentHashMap<>();
// apkPath, PreLoadedApk
private final Map<String, PreLoadedApk> cachedApkFile = new ConcurrentHashMap<>();
// appId, packageName
private final Map<Integer, String> cachedModule = new ConcurrentHashMap<>();
// packageName, Module
private final Map<String, Module> cachedModule = new ConcurrentHashMap<>();
// packageName, userId, group, key, value
private final Map<Pair<String, Integer>, Map<String, ConcurrentHashMap<String, Object>>> 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<String, Map<Integer, PackageInfo>> modules = new HashMap<>();
int apkPathIdx = cursor.getColumnIndex("apk_path");
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()) {
String packageName = cursor.getString(pkgNameIdx);
int userId = cursor.getInt(userIdIdx);
var pkgInfo = modules.computeIfAbsent(packageName, m -> {
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 {
return PackageService.getPackageInfoFromAllUsers(m, 0);
pkgInfo = PackageService.getPackageInfo(packageName, MATCH_ALL_FLAGS, 0);
} catch (Throwable e) {
Log.e(TAG, Log.getStackTraceString(e));
return Collections.emptyMap();
Log.w(TAG, "get package info of " + packageName, e);
}
});
if (pkgInfo.isEmpty()) {
if (pkgInfo == null || pkgInfo.applicationInfo == null) {
obsoleteModules.add(packageName);
} else if (!pkgInfo.containsKey(userId)) {
var module = new Application();
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.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);
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<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()) {
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<ProcessScope> 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<Module> 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 {

View File

@ -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;
}

View File

@ -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();

View File

@ -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;
}