Skip to content

Commit

Permalink
Custom ServiceLoader without jar checksum verification
Browse files Browse the repository at this point in the history
Fixes #878
  • Loading branch information
SokolovaMaria committed Mar 18, 2019
1 parent 8273a75 commit d8aa178
Show file tree
Hide file tree
Showing 15 changed files with 168 additions and 13 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ Make sure that you have either `jcenter()` or `mavenCentral()` in the list of re

Core modules of `kotlinx.coroutines` are also available for
[Kotlin/JS](js/README.md) and [Kotlin/Native](native/README.md). If you write
a common code that should get compiled or different platforms, add
a common code that should get compiled for different platforms, add
[`org.jetbrains.kotlinx:kotlinx-coroutines-core-common:<version>`](common/kotlinx-coroutines-core-common/README.md)
to your common code dependencies.

Expand Down
1 change: 0 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ configure(subprojects.findAll { !sourceless.contains(it.name) && it.name != "ben
test.kotlin.srcDirs = ['test']
main.resources.srcDirs = ['resources']
test.resources.srcDirs = ['test-resources']

}
}

Expand Down
3 changes: 2 additions & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#Fri Mar 15 12:06:46 CET 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-all.zip
2 changes: 2 additions & 0 deletions kotlinx-coroutines-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ kotlin.sourceSets {
jvmTest.dependencies {
api "com.devexperts.lincheck:lincheck:$lincheck_version"
api "com.esotericsoftware:kryo:4.0.0"

implementation project (":android-unit-tests")
}
}

Expand Down
4 changes: 2 additions & 2 deletions kotlinx-coroutines-core/common/src/Await.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import kotlin.jvm.*
* when all deferred computations are complete or resumes with the first thrown exception if any of computations
* complete exceptionally including cancellation.
*
* This function is **not** equivalent to `deferreds.map { it.await() }` which fails only when when it sequentially
* gets to wait the failing deferred, while this `awaitAll` fails immediately as soon as any of the deferreds fail.
* This function is **not** equivalent to `deferreds.map { it.await() }` which fails only when it sequentially
* gets to wait for the failing deferred, while this `awaitAll` fails immediately as soon as any of the deferreds fail.
*
* This suspending function is cancellable.
* If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
kotlinx.coroutines.android.EmptyCoroutineScopeImpl1
kotlinx.coroutines.android.EmptyCoroutineScopeImpl2
# testing configuration file parsing # kotlinx.coroutines.service.loader.LocalEmptyCoroutineScope2

kotlinx.coroutines.android.EmptyCoroutineScopeImpl2

kotlinx.coroutines.android.EmptyCoroutineScopeImpl1


kotlinx.coroutines.android.EmptyCoroutineScopeImpl1


kotlinx.coroutines.android.EmptyCoroutineScopeImpl3#comment
2 changes: 1 addition & 1 deletion kotlinx-coroutines-core/jvm/src/Builders.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import kotlin.coroutines.*
* This function should not be used from coroutine. It is designed to bridge regular blocking code
* to libraries that are written in suspending style, to be used in `main` functions and in tests.
*
* The default [CoroutineDispatcher] for this builder in an internal implementation of event loop that processes continuations
* The default [CoroutineDispatcher] for this builder is an internal implementation of event loop that processes continuations
* in this blocked thread until the completion of this coroutine.
* See [CoroutineDispatcher] for the other implementations that are provided by `kotlinx.coroutines`.
*
Expand Down
87 changes: 87 additions & 0 deletions kotlinx-coroutines-core/jvm/src/internal/FastServiceLoader.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package kotlinx.coroutines.internal

import java.util.*
import java.io.*
import java.net.*
import java.util.jar.*
import java.util.zip.*

/**
* Name of the boolean property that enables using of [FastServiceLoader].
*/
private const val FAST_SERVICE_LOADER_PROPERTY_NAME = "kotlinx.coroutines.verify.service.loader"

/**
* A simplified version of [ServiceLoader].
* FastServiceLoader locates and instantiates all service providers named in configuration
* files placed in the resource directory <tt>META-INF/services</tt>.
*
* The main difference between this class and classic service loader is in skipping
* verification JARs. A verification requires reading the whole JAR (and it causes problems and ANRs on Android devices)
* and prevents only trivial checksum issues. See #878.
*
* If any error occurs during loading, it fallbacks to [ServiceLoader], mostly to prevent R8 issues.
*/

internal object FastServiceLoader {
private const val PREFIX: String = "META-INF/services/"

@JvmField
internal val FAST_SERVICE_LOADER_ENABLED = systemProp(FAST_SERVICE_LOADER_PROPERTY_NAME, true)

internal fun <S> load(service: Class<S>, loader: ClassLoader): List<S> {
if (!FAST_SERVICE_LOADER_ENABLED) {
return ServiceLoader.load(service, loader).toList()
}
return try {
loadProviders(service, loader)
} catch (e: Throwable) {
// Fallback to default service loader
ServiceLoader.load(service, loader).toList()
}
}

internal fun <S> loadProviders(service: Class<S>, loader: ClassLoader): List<S> {
val fullServiceName = PREFIX + service.name
val urls = loader.getResources(fullServiceName).toList()
val providers = mutableListOf<S>()
urls.forEach {
val providerNames = parse(it)
providers.addAll(providerNames.map { getProviderInstance(it, loader, service) })
}
require(providers.isNotEmpty()) { "No providers were loaded with FastServiceLoader" }
return providers
}

private fun <S> getProviderInstance(name: String, loader: ClassLoader, service: Class<S>): S {
val clazz = Class.forName(name, false, loader)
require(service.isAssignableFrom(clazz)) { "Expected service of class $service, but found $clazz" }
return service.cast(clazz.getDeclaredConstructor().newInstance())
}

private fun parse(url: URL): List<String> {
val string = url.toString()
return if (string.startsWith("jar")) {
val pathToJar = string.substringAfter("jar:file:").substringBefore('!')
val entry = string.substringAfter("!/")
(JarFile(pathToJar, false) as Closeable).use { file ->
BufferedReader(InputStreamReader((file as JarFile).getInputStream(ZipEntry(entry)),"UTF-8")).use { r ->
parseFile(r)
}
}
} else emptyList()
}

private fun parseFile(r: BufferedReader): List<String> {
val names = mutableSetOf<String>()
while (true) {
val line = r.readLine() ?: break
val serviceName = line.substringBefore("#").trim()
require(serviceName.all { it == '.' || Character.isJavaIdentifierPart(it) }) { "Illegal service provider class name: $serviceName" }
if (serviceName.isNotEmpty()) {
names.add(serviceName)
}
}
return names.toList()
}
}
9 changes: 4 additions & 5 deletions kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ internal object MainDispatcherLoader {
private fun loadMainDispatcher(): MainCoroutineDispatcher {
return try {
val factories = MainDispatcherFactory::class.java.let { clz ->
ServiceLoader.load(clz, clz.classLoader).toList()
FastServiceLoader.load(clz, clz.classLoader)
}

factories.maxBy { it.loadPriority }?.tryCreateDispatcher(factories)
?: MissingMainCoroutineDispatcher(null)
} catch (e: Throwable) {
Expand Down Expand Up @@ -72,7 +71,7 @@ private class MissingMainCoroutineDispatcher(
if (cause == null) {
throw IllegalStateException(
"Module with the Main dispatcher is missing. " +
"Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android'"
"Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android'"
)
} else {
val message = "Module with the Main dispatcher had failed to initialize" + (errorHint?.let { ". $it" } ?: "")
Expand All @@ -92,6 +91,6 @@ public object MissingMainCoroutineDispatcherFactory : MainDispatcherFactory {
get() = -1

override fun createDispatcher(allFactories: List<MainDispatcherFactory>): MainCoroutineDispatcher {
return MissingMainCoroutineDispatcher(null)
return MissingMainCoroutineDispatcher(null)
}
}
}
23 changes: 23 additions & 0 deletions kotlinx-coroutines-core/jvm/test/internal/ServiceLoaderTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package kotlinx.coroutines.internal

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Delay
import kotlin.test.Test

class ServiceLoaderTest {
@Test
fun testLoadingSameModuleService() {
val providers = Delay::class.java.let { FastServiceLoader.loadProviders(it, it.classLoader) }
assert(providers.size == 1 && providers[0].javaClass.name == "kotlinx.coroutines.android.DelayImpl")
}

@Test
fun testCrossModuleService() {
val providers = CoroutineScope::class.java.let { FastServiceLoader.loadProviders(it, it.classLoader) }
assert(providers.size == 3)
val className = "kotlinx.coroutines.android.EmptyCoroutineScopeImpl"
for (i in 1 .. 3) {
assert(providers[i - 1].javaClass.name == "$className$i")
}
}
}
2 changes: 1 addition & 1 deletion license/NOTICE.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
=========================================================================
== NOTICE file corresponding to the section 4 d of ==
== the Apache License, Version 2.0, ==
== in this case for the kotlix.coroutines library. ==
== in this case for the kotlinx.coroutines library. ==
=========================================================================

kotlinx.coroutines library.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
kotlinx.coroutines.android.DelayImpl
10 changes: 10 additions & 0 deletions ui/kotlinx-coroutines-android/android-unit-tests/src/DelayImpl.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package kotlinx.coroutines.android

import kotlinx.coroutines.*

@InternalCoroutinesApi
class DelayImpl : Delay {
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
continuation.cancel()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package kotlinx.coroutines.android

import kotlinx.coroutines.CoroutineScope
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext

internal class EmptyCoroutineScopeImpl1 : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}

internal class EmptyCoroutineScopeImpl2 : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}

internal class EmptyCoroutineScopeImpl3 : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
kotlinx.coroutines.android.AndroidDispatcherFactory
kotlinx.coroutines.android.AndroidDispatcherFactory

0 comments on commit d8aa178

Please sign in to comment.