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..66e2eb7e 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,114 @@ 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: + ${exception.stackTraceToString()} + """.trimIndent(), + ), + ) + } + } + } + + 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: + ${result.exception.stackTraceToString()} + """.trimIndent(), + 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..7a48ffe7 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt @@ -108,9 +108,11 @@ 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) { + 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..8082704c 100644 --- a/src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt +++ b/src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt @@ -2,14 +2,15 @@ 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.