Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ object ConfigCache {
packageName = pkgName
this.apkPath = apkPath
appId = appInfo.uid
versionCode = pkgInfo.longVersionCode
applicationInfo = appInfo
service = oldModule?.service ?: InjectedModuleService(pkgName)
file = preLoadedApk
Expand Down Expand Up @@ -340,6 +341,8 @@ object ConfigCache {
fun getModuleByUid(uid: Int): Module? =
state.modules.values.firstOrNull { it.appId == uid % PER_USER_RANGE }

fun getModuleByPackage(packageName: String): Module? = state.modules[packageName]

fun getModulesForSystemServer(): List<Module> {
val modules = mutableListOf<Module>()
if (!android.os.SELinux.checkSELinuxAccess(
Expand Down Expand Up @@ -376,6 +379,7 @@ object ConfigCache {
packageName = pkgName
this.apkPath = apkPath
appId = runCatching { Os.stat(statPath).st_uid }.getOrDefault(-1)
versionCode = 0
service = InjectedModuleService(pkgName)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import android.os.ParcelFileDescriptor
import android.os.Process
import android.os.RemoteException
import android.util.Log
import io.github.libxposed.service.HookedProcess
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicLong
import org.lsposed.lspd.models.Module
import org.lsposed.lspd.service.ILSPApplicationService
import org.lsposed.lspd.service.IHotReloadTarget
import org.matrix.vector.daemon.data.ConfigCache
import org.matrix.vector.daemon.data.FileSystem
import org.matrix.vector.daemon.utils.InstallerVerifier
Expand All @@ -29,6 +32,8 @@ object ApplicationService : ILSPApplicationService.Stub() {
data class ProcessKey(val uid: Int, val pid: Int)

private val processes = ConcurrentHashMap<ProcessKey, ProcessInfo>()
private val nextHotReloadTargetId = AtomicLong(1)
private val hotReloadTargets = ConcurrentHashMap<Long, HotReloadTargetInfo>()

private class ProcessInfo(val key: ProcessKey, val processName: String, val heartBeat: IBinder) :
IBinder.DeathRecipient {
Expand All @@ -40,6 +45,45 @@ object ApplicationService : ILSPApplicationService.Stub() {
override fun binderDied() {
heartBeat.unlinkToDeath(this, 0)
processes.remove(key)
hotReloadTargets.entries.removeIf { it.value.process === this }
}
}

private class HotReloadTargetInfo(
val id: Long,
val modulePackageName: String,
val process: ProcessInfo,
@Volatile var loadedVersionCode: Long,
val target: IHotReloadTarget
) : IBinder.DeathRecipient {
@Volatile var state: Int = HookedProcess.TARGET_STATE_UP_TO_DATE

init {
target.asBinder().linkToDeath(this, 0)
hotReloadTargets[id] = this
}

override fun binderDied() {
target.asBinder().unlinkToDeath(this, 0)
hotReloadTargets.remove(id)
}

fun toHookedProcess(currentVersionCode: Long): HookedProcess {
val effectiveState =
if (state == HookedProcess.TARGET_STATE_UP_TO_DATE &&
loadedVersionCode != currentVersionCode) {
HookedProcess.TARGET_STATE_STALE
} else {
state
}
return HookedProcess().apply {
targetId = id
uid = process.key.uid
pid = process.key.pid
processName = process.processName
state = effectiveState
loadedVersionCode = this@HotReloadTargetInfo.loadedVersionCode
}
}
}

Expand Down Expand Up @@ -129,4 +173,64 @@ object ApplicationService : ILSPApplicationService.Stub() {
.onFailure { Log.e(TAG, "Failed to open or verify manager APK", it) }
.getOrNull()
}

override fun registerHotReloadTarget(
modulePackageName: String,
loadedVersionCode: Long,
target: IHotReloadTarget
): Long {
val info = ensureRegistered()
val module =
ConfigCache.getModuleByPackage(modulePackageName)
?: throw RemoteException("Unknown module: $modulePackageName")
if (!getAllModules().any { it.packageName == module.packageName }) {
throw RemoteException("Module $modulePackageName is not active in ${info.processName}")
}

val existing =
hotReloadTargets.values.firstOrNull {
it.modulePackageName == modulePackageName &&
it.process.key == info.key &&
it.target.asBinder() == target.asBinder()
}
if (existing != null) {
existing.loadedVersionCode = loadedVersionCode
existing.state = HookedProcess.TARGET_STATE_UP_TO_DATE
return existing.id
}

val id = nextHotReloadTargetId.getAndIncrement()
HotReloadTargetInfo(id, module.packageName, info, loadedVersionCode, target)
return id
}

fun getRunningTargets(module: Module): List<HookedProcess> {
return hotReloadTargets.values
.filter { it.modulePackageName == module.packageName }
.map { it.toHookedProcess(module.versionCode) }
}

fun hotReloadTarget(targetId: Long, module: Module, extras: android.os.Bundle?) {
val target =
hotReloadTargets[targetId] ?: throw SecurityException("Invalid hot reload target: $targetId")
if (target.modulePackageName != module.packageName) {
throw SecurityException("Target $targetId does not belong to ${module.packageName}")
}
if (target.state == HookedProcess.TARGET_STATE_RELOADING) {
throw IllegalStateException("Target $targetId is already reloading")
}

target.state = HookedProcess.TARGET_STATE_RELOADING
runCatching {
target.target.hotReloadModule(module, extras)
target.loadedVersionCode = module.versionCode
target.state = HookedProcess.TARGET_STATE_UP_TO_DATE
}
.onFailure {
target.state =
if (target.target.asBinder().isBinderAlive) HookedProcess.TARGET_STATE_FAILED
else HookedProcess.TARGET_STATE_FAILED
throw it
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import android.os.Bundle
import android.os.ParcelFileDescriptor
import android.os.RemoteException
import android.util.Log
import io.github.libxposed.service.HookedProcess
import io.github.libxposed.service.IHotReloadCallback
import io.github.libxposed.service.IXposedScopeCallback
import io.github.libxposed.service.IXposedService
import java.io.Serializable
Expand Down Expand Up @@ -110,6 +112,9 @@ class ModuleService(private val loadedModule: Module) : IXposedService.Stub() {
override fun getFrameworkProperties(): Long {
ensureModule()
var prop = IXposedService.PROP_CAP_SYSTEM or IXposedService.PROP_CAP_REMOTE
if (loadedModule.file.moduleClassNames.size == 1) {
prop = prop or IXposedService.PROP_RT_HOT_RELOAD
}
if (ConfigCache.state.isDexObfuscateEnabled)
prop = prop or IXposedService.PROP_RT_API_PROTECTION
return prop
Expand Down Expand Up @@ -140,6 +145,37 @@ class ModuleService(private val loadedModule: Module) : IXposedService.Stub() {
}
}

override fun getRunningTargets(): List<HookedProcess> {
ensureModule()
return ApplicationService.getRunningTargets(loadedModule)
}

override fun hotReloadModule(
targetId: Long,
data: Bundle?,
callback: IHotReloadCallback?
) {
ensureModule()
runCatching {
if (loadedModule.file.moduleClassNames.size != 1) {
throw SecurityException("Hot reload requires exactly one Java entry class")
}
val latest =
ConfigCache.getModuleByPackage(loadedModule.packageName)
?: throw SecurityException("Module ${loadedModule.packageName} is not enabled")
ApplicationService.hotReloadTarget(targetId, latest, data)
callback?.onHotReloadResult(IXposedService.HOT_RELOAD_SUCCESS, null)
}
.onFailure { throwable ->
val status =
when (throwable) {
is IllegalStateException -> IXposedService.HOT_RELOAD_IN_PROGRESS
else -> IXposedService.HOT_RELOAD_FAILED
}
callback?.onHotReloadResult(status, throwable.message)
}
}

override fun requestRemotePreferences(group: String): Bundle {
val userId = ensureModule()
return Bundle().apply {
Expand Down
1 change: 1 addition & 0 deletions services/daemon-service/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ android {

dependencies {
compileOnly(libs.androidx.annotation)
compileOnly(projects.shared.libxposedAnnotation)
compileOnly(projects.hiddenapi.stubs)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.lsposed.lspd.service.ILSPInjectedModuleService;
parcelable Module {
String packageName;
int appId;
long versionCode;
String apkPath;
PreLoadedApk file;
ApplicationInfo applicationInfo;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.lsposed.lspd.service;

import org.lsposed.lspd.models.Module;

interface IHotReloadTarget {
void hotReloadModule(in Module module, in Bundle extras);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.lsposed.lspd.service;

import org.lsposed.lspd.models.Module;
import org.lsposed.lspd.service.IHotReloadTarget;

interface ILSPApplicationService {
boolean isLogMuted();
Expand All @@ -12,4 +13,6 @@ interface ILSPApplicationService {
String getPrefsPath(String packageName);

ParcelFileDescriptor requestInjectedManagerBinder(out List<IBinder> binder);

long registerHotReloadTarget(String modulePackageName, long loadedVersionCode, IHotReloadTarget target);
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ include(
":hiddenapi:stubs",
":hiddenapi:bridge",
":legacy",
":shared:libxposed-annotation",
":services:manager-service",
":services:daemon-service",
":xposed",
Expand Down
6 changes: 6 additions & 0 deletions shared/libxposed-annotation/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
plugins { `java-library` }

java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.github.libxposed.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.CLASS)
@Target({
ElementType.TYPE,
ElementType.FIELD,
ElementType.METHOD,
ElementType.CONSTRUCTOR,
})
public @interface InternalApi {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.github.libxposed.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.CLASS)
@Target({
ElementType.TYPE,
ElementType.FIELD,
ElementType.METHOD,
ElementType.CONSTRUCTOR,
ElementType.PARAMETER,
})
public @interface SinceApi {
int value();
}
1 change: 1 addition & 0 deletions xposed/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ dependencies {
implementation(projects.hiddenapi.bridge)
implementation(projects.services.daemonService)
compileOnly(libs.androidx.annotation)
compileOnly(projects.shared.libxposedAnnotation)
compileOnly(projects.hiddenapi.stubs)
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ class VectorContext(
}

override fun hook(origin: Executable): XposedInterface.HookBuilder {
return VectorHookBuilder(origin)
return VectorHookBuilder(packageName, origin)
}

override fun hookClassInitializer(origin: Class<*>): XposedInterface.HookBuilder {
val clinit =
HookBridge.getStaticInitializer(origin)
?: throw IllegalArgumentException("Class ${origin.name} has no static initializer")
return VectorHookBuilder(clinit)
return VectorHookBuilder(packageName, clinit)
}

override fun deoptimize(executable: Executable): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ object VectorLifecycleManager {

val activeModules: MutableSet<XposedModule> = ConcurrentHashMap.newKeySet()

fun detach(module: XposedModule) {
activeModules.remove(module)
}

fun dispatchPackageLoaded(
packageName: String,
appInfo: ApplicationInfo,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.matrix.vector.impl.core

import android.os.Bundle
import org.lsposed.lspd.models.Module
import org.lsposed.lspd.service.IHotReloadTarget

internal object VectorHotReloadTarget : IHotReloadTarget.Stub() {
override fun hotReloadModule(module: Module, extras: Bundle?) {
VectorModuleManager.hotReloadModule(module, extras)
}
}
Loading