diff --git a/src/main/kotlin/app/revanced/patcher/Fingerprint.kt b/src/main/kotlin/app/revanced/patcher/Fingerprint.kt index a44c00ba..b329d5f8 100644 --- a/src/main/kotlin/app/revanced/patcher/Fingerprint.kt +++ b/src/main/kotlin/app/revanced/patcher/Fingerprint.kt @@ -84,9 +84,11 @@ class Fingerprint internal constructor( } if (match != null) return match - classes.forEach { classDef -> - match = matchOrNull(classDef) - if (match != null) return match + synchronized(classes) { + classes.forEach { classDef -> + match = matchOrNull(classDef) + if (match != null) return match + } } return null diff --git a/src/main/kotlin/app/revanced/patcher/Patcher.kt b/src/main/kotlin/app/revanced/patcher/Patcher.kt index e7d49157..1cfaa6d5 100644 --- a/src/main/kotlin/app/revanced/patcher/Patcher.kt +++ b/src/main/kotlin/app/revanced/patcher/Patcher.kt @@ -1,8 +1,10 @@ package app.revanced.patcher import app.revanced.patcher.patch.* -import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.channelFlow import java.io.Closeable +import java.util.concurrent.ConcurrentHashMap import java.util.logging.Logger /** @@ -56,41 +58,7 @@ class Patcher(private val config: PatcherConfig) : Closeable { * * @return A flow of [PatchResult]s. */ - operator fun invoke() = flow { - fun Patch<*>.execute( - executedPatches: LinkedHashMap, PatchResult>, - ): PatchResult { - // If the patch was executed before or failed, return it's the result. - executedPatches[this]?.let { patchResult -> - patchResult.exception ?: return patchResult - - return PatchResult(this, PatchException("The patch '$this' failed previously")) - } - - // Recursively execute all dependency patches. - dependencies.forEach { dependency -> - dependency.execute(executedPatches).exception?.let { - return PatchResult( - this, - PatchException( - "The patch \"$this\" depends on \"$dependency\", which raised an exception:\n${it.stackTraceToString()}", - ), - ) - } - } - - // Execute the patch. - return try { - execute(context) - - PatchResult(this) - } catch (exception: PatchException) { - PatchResult(this, exception) - } catch (exception: Exception) { - PatchResult(this, PatchException(exception)) - }.also { executedPatches[this] = it } - } - + operator fun invoke() = channelFlow { // Prevent decoding the app manifest twice if it is not needed. if (config.resourceMode != ResourcePatchContext.ResourceMode.NONE) { context.resourceContext.decodeResources(config.resourceMode) @@ -103,48 +71,110 @@ class Patcher(private val config: PatcherConfig) : Closeable { logger.info("Executing patches") - val executedPatches = LinkedHashMap, PatchResult>() + val executedPatches = ConcurrentHashMap, Deferred>() - context.executablePatches.sortedBy { it.name }.forEach { patch -> - val patchResult = patch.execute(executedPatches) + suspend operator fun Patch<*>.invoke(): Deferred { + val patch = this - // If an exception occurred or the patch has no finalize block, emit the result. - if (patchResult.exception != null || patch.finalizeBlock == null) { - emit(patchResult) + // If the patch was executed before or failed, return it's the result. + executedPatches[patch]?.let { deferredPatchResult -> + val patchResult = deferredPatchResult.await() + + patchResult.exception ?: return deferredPatchResult + + return CompletableDeferred(PatchResult(patch, PatchException("The patch '$patch' failed previously"))) } - } - val succeededPatchesWithFinalizeBlock = executedPatches.values.filter { - it.exception == null && it.patch.finalizeBlock != null - } + return async(Dispatchers.IO) { + // Recursively execute all dependency patches. + val dependenciesResult = coroutineScope { + val dependenciesJobs = dependencies.map { dependency -> + async(Dispatchers.IO) { + dependency().await().exception?.let { exception -> + PatchResult( + patch, + PatchException( + "The patch \"$patch\" depends on \"$dependency\", which raised an exception:\n" + + exception.stackTraceToString(), + ), + ) + } + } + } + + dependenciesJobs.awaitAll().firstOrNull { result -> result != null }?.let { + dependenciesJobs.forEach(Deferred<*>::cancel) + + return@coroutineScope it + } + } - succeededPatchesWithFinalizeBlock.asReversed().forEach { executionResult -> - val patch = executionResult.patch + if (dependenciesResult != null) { + return@async dependenciesResult + } - val result = + // Execute the patch. try { - patch.finalize(context) + execute(context) - executionResult + PatchResult(patch) } catch (exception: PatchException) { PatchResult(patch, exception) } catch (exception: Exception) { PatchResult(patch, PatchException(exception)) } + }.also { executedPatches[patch] = it } + } - if (result.exception != null) { - emit( - PatchResult( - patch, - PatchException( - "The patch \"$patch\" raised an exception: ${result.exception.stackTraceToString()}", - result.exception, - ), - ), - ) - } else if (patch in context.executablePatches) { - emit(result) - } + coroutineScope { + context.executablePatches.sortedBy { it.name }.map { patch -> + launch(Dispatchers.IO) { + val patchResult = patch().await() + + // If an exception occurred or the patch has no finalize block, emit the result. + if (patchResult.exception != null || patch.finalizeBlock == null) { + send(patchResult) + } + } + }.joinAll() + } + + val succeededPatchesWithFinalizeBlock = executedPatches.values.map { it.await() }.filter { + it.exception == null && it.patch.finalizeBlock != null + } + + coroutineScope { + succeededPatchesWithFinalizeBlock.asReversed().map { executionResult -> + launch(Dispatchers.IO) { + val patch = executionResult.patch + + val result = + try { + patch.finalize(context) + + executionResult + } catch (exception: PatchException) { + PatchResult(patch, exception) + } catch (exception: Exception) { + PatchResult(patch, PatchException(exception)) + } + + if (result.exception != null) { + send( + PatchResult( + patch, + PatchException( + "The patch \"$patch\" raised an exception during finalization:\n" + + result.exception.stackTraceToString(), + result.exception, + ), + ), + ) + } else if (patch in context.executablePatches) { + send(result) + } + } + }.joinAll() } } diff --git a/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt b/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt index b5a14ac7..b243153c 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt @@ -98,8 +98,17 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi * @param predicate A predicate to match the class. * @return A proxy for the first class that matches the predicate. */ - fun classBy(predicate: (ClassDef) -> Boolean) = - classes.proxyPool.find { predicate(it.immutableClass) } ?: classes.find(predicate)?.let { proxy(it) } + fun classBy(predicate: (ClassDef) -> Boolean): ClassProxy? { + val proxy = synchronized(classes.proxyPool) { classes.proxyPool.find { predicate(it.immutableClass) } } + if (proxy != null) return proxy + + val classDef = synchronized(classes) { classes.find(predicate) } + if (classDef != null) { + return proxy(classDef) + } + + return null + } /** * Proxy the class to allow mutation. @@ -108,9 +117,10 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi * * @return A proxy for the class. */ - fun proxy(classDef: ClassDef) = classes.proxyPool.find { - it.immutableClass.type == classDef.type - } ?: ClassProxy(classDef).also { classes.proxyPool.add(it) } + fun proxy(classDef: ClassDef) = synchronized(classes.proxyPool) { + classes.proxyPool.find { it.immutableClass.type == classDef.type } + ?: ClassProxy(classDef).also { classes.proxyPool.add(it) } + } /** * Navigate a method. diff --git a/src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt b/src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt index cdc334f8..e89da661 100644 --- a/src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt +++ b/src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt @@ -2,19 +2,22 @@ package app.revanced.patcher.util import app.revanced.patcher.util.proxy.ClassProxy import com.android.tools.smali.dexlib2.iface.ClassDef +import java.util.* /** * A list of classes and proxies. * * @param classes The classes to be backed by proxies. */ -class ProxyClassList internal constructor(classes: MutableList) : MutableList by classes { - internal val proxyPool = mutableListOf() +class ProxyClassList internal constructor( + classes: MutableList, +) : MutableList by Collections.synchronizedList(classes) { + internal val proxyPool = Collections.synchronizedList(mutableListOf()) /** * Replace all classes with their mutated versions. */ - internal fun replaceClasses() = + internal fun replaceClasses() { proxyPool.removeIf { proxy -> // If the proxy is unused, return false to keep it in the proxies list. if (!proxy.resolved) return@removeIf false @@ -26,4 +29,5 @@ class ProxyClassList internal constructor(classes: MutableList) : Muta // Return true to remove the proxy from the proxies list. return@removeIf true } + } } diff --git a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt index bf4aab27..4da68b09 100644 --- a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt +++ b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt @@ -64,9 +64,9 @@ internal object PatcherTest { patches() assertEquals( - listOf("1", "2", "3", "4", "-1", "-2"), - executed, - "Expected patches to be executed in correct order.", + setOf("1", "2", "3", "4", "-1", "-2"), + executed.toSet(), + "Unexpected patch results", ) }