Skip to content

Commit 15b6345

Browse files
SokolovaMariaqwwdfsad
authored andcommitted
Custom ServiceLoader without jar checksum verification
Fixes #878
1 parent 0d69e13 commit 15b6345

File tree

11 files changed

+163
-8
lines changed

11 files changed

+163
-8
lines changed

build.gradle

-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,6 @@ configure(subprojects.findAll { !sourceless.contains(it.name) && it.name != "ben
133133
test.kotlin.srcDirs = ['test']
134134
main.resources.srcDirs = ['resources']
135135
test.resources.srcDirs = ['test-resources']
136-
137136
}
138137
}
139138

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
#Fri Mar 15 12:06:46 CET 2019
12
distributionBase=GRADLE_USER_HOME
23
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-bin.zip
44
zipStoreBase=GRADLE_USER_HOME
55
zipStorePath=wrapper/dists
6+
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-all.zip

kotlinx-coroutines-core/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ kotlin.sourceSets {
4646
jvmTest.dependencies {
4747
api "com.devexperts.lincheck:lincheck:$lincheck_version"
4848
api "com.esotericsoftware:kryo:4.0.0"
49+
50+
implementation project (":android-unit-tests")
4951
}
5052
}
5153

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
kotlinx.coroutines.android.EmptyCoroutineScopeImpl1
2+
kotlinx.coroutines.android.EmptyCoroutineScopeImpl2
3+
# testing configuration file parsing # kotlinx.coroutines.service.loader.LocalEmptyCoroutineScope2
4+
5+
kotlinx.coroutines.android.EmptyCoroutineScopeImpl2
6+
7+
kotlinx.coroutines.android.EmptyCoroutineScopeImpl1
8+
9+
10+
kotlinx.coroutines.android.EmptyCoroutineScopeImpl1
11+
12+
13+
kotlinx.coroutines.android.EmptyCoroutineScopeImpl3#comment
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package kotlinx.coroutines.internal
2+
3+
import java.util.*
4+
import java.io.*
5+
import java.net.*
6+
import java.util.jar.*
7+
import java.util.zip.*
8+
9+
/**
10+
* Name of the boolean property that enables using of [FastServiceLoader].
11+
*/
12+
private const val FAST_SERVICE_LOADER_PROPERTY_NAME = "kotlinx.coroutines.verify.service.loader"
13+
14+
/**
15+
* A simplified version of [ServiceLoader].
16+
* FastServiceLoader locates and instantiates all service providers named in configuration
17+
* files placed in the resource directory <tt>META-INF/services</tt>.
18+
*
19+
* The main difference between this class and classic service loader is in skipping
20+
* verification JARs. A verification requires reading the whole JAR (and it causes problems and ANRs on Android devices)
21+
* and prevents only trivial checksum issues. See #878.
22+
*
23+
* If any error occurs during loading, it fallbacks to [ServiceLoader], mostly to prevent R8 issues.
24+
*/
25+
26+
internal object FastServiceLoader {
27+
private const val PREFIX: String = "META-INF/services/"
28+
29+
@JvmField
30+
internal val FAST_SERVICE_LOADER_ENABLED = systemProp(FAST_SERVICE_LOADER_PROPERTY_NAME, true)
31+
32+
internal fun <S> load(service: Class<S>, loader: ClassLoader): List<S> {
33+
if (!FAST_SERVICE_LOADER_ENABLED) {
34+
return ServiceLoader.load(service, loader).toList()
35+
}
36+
return try {
37+
loadProviders(service, loader)
38+
} catch (e: Throwable) {
39+
// Fallback to default service loader
40+
ServiceLoader.load(service, loader).toList()
41+
}
42+
}
43+
44+
internal fun <S> loadProviders(service: Class<S>, loader: ClassLoader): List<S> {
45+
val fullServiceName = PREFIX + service.name
46+
val urls = loader.getResources(fullServiceName).toList()
47+
val providers = mutableListOf<S>()
48+
urls.forEach {
49+
val providerNames = parse(it)
50+
providers.addAll(providerNames.map { getProviderInstance(it, loader, service) })
51+
}
52+
require(providers.isNotEmpty()) { "No providers were loaded with FastServiceLoader" }
53+
return providers
54+
}
55+
56+
private fun <S> getProviderInstance(name: String, loader: ClassLoader, service: Class<S>): S {
57+
val clazz = Class.forName(name, false, loader)
58+
require(service.isAssignableFrom(clazz)) { "Expected service of class $service, but found $clazz" }
59+
return service.cast(clazz.getDeclaredConstructor().newInstance())
60+
}
61+
62+
private fun parse(url: URL): List<String> {
63+
val string = url.toString()
64+
return if (string.startsWith("jar")) {
65+
val pathToJar = string.substringAfter("jar:file:").substringBefore('!')
66+
val entry = string.substringAfter("!/")
67+
(JarFile(pathToJar, false) as Closeable).use { file ->
68+
BufferedReader(InputStreamReader((file as JarFile).getInputStream(ZipEntry(entry)),"UTF-8")).use { r ->
69+
parseFile(r)
70+
}
71+
}
72+
} else emptyList()
73+
}
74+
75+
private fun parseFile(r: BufferedReader): List<String> {
76+
val names = mutableSetOf<String>()
77+
while (true) {
78+
val line = r.readLine() ?: break
79+
val serviceName = line.substringBefore("#").trim()
80+
require(serviceName.all { it == '.' || Character.isJavaIdentifierPart(it) }) { "Illegal service provider class name: $serviceName" }
81+
if (serviceName.isNotEmpty()) {
82+
names.add(serviceName)
83+
}
84+
}
85+
return names.toList()
86+
}
87+
}

kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt

+4-5
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@ internal object MainDispatcherLoader {
1212
private fun loadMainDispatcher(): MainCoroutineDispatcher {
1313
return try {
1414
val factories = MainDispatcherFactory::class.java.let { clz ->
15-
ServiceLoader.load(clz, clz.classLoader).toList()
15+
FastServiceLoader.load(clz, clz.classLoader)
1616
}
17-
1817
factories.maxBy { it.loadPriority }?.tryCreateDispatcher(factories)
1918
?: MissingMainCoroutineDispatcher(null)
2019
} catch (e: Throwable) {
@@ -72,7 +71,7 @@ private class MissingMainCoroutineDispatcher(
7271
if (cause == null) {
7372
throw IllegalStateException(
7473
"Module with the Main dispatcher is missing. " +
75-
"Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android'"
74+
"Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android'"
7675
)
7776
} else {
7877
val message = "Module with the Main dispatcher had failed to initialize" + (errorHint?.let { ". $it" } ?: "")
@@ -92,6 +91,6 @@ public object MissingMainCoroutineDispatcherFactory : MainDispatcherFactory {
9291
get() = -1
9392

9493
override fun createDispatcher(allFactories: List<MainDispatcherFactory>): MainCoroutineDispatcher {
95-
return MissingMainCoroutineDispatcher(null)
94+
return MissingMainCoroutineDispatcher(null)
9695
}
97-
}
96+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package kotlinx.coroutines.internal
2+
3+
import kotlinx.coroutines.CoroutineScope
4+
import kotlinx.coroutines.Delay
5+
import kotlin.test.Test
6+
7+
class ServiceLoaderTest {
8+
@Test
9+
fun testLoadingSameModuleService() {
10+
val providers = Delay::class.java.let { FastServiceLoader.loadProviders(it, it.classLoader) }
11+
assert(providers.size == 1 && providers[0].javaClass.name == "kotlinx.coroutines.android.DelayImpl")
12+
}
13+
14+
@Test
15+
fun testCrossModuleService() {
16+
val providers = CoroutineScope::class.java.let { FastServiceLoader.loadProviders(it, it.classLoader) }
17+
assert(providers.size == 3)
18+
val className = "kotlinx.coroutines.android.EmptyCoroutineScopeImpl"
19+
for (i in 1 .. 3) {
20+
assert(providers[i - 1].javaClass.name == "$className$i")
21+
}
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
kotlinx.coroutines.android.DelayImpl
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package kotlinx.coroutines.android
2+
3+
import kotlinx.coroutines.*
4+
5+
@InternalCoroutinesApi
6+
class DelayImpl : Delay {
7+
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
8+
continuation.cancel()
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package kotlinx.coroutines.android
2+
3+
import kotlinx.coroutines.CoroutineScope
4+
import kotlin.coroutines.CoroutineContext
5+
import kotlin.coroutines.EmptyCoroutineContext
6+
7+
internal class EmptyCoroutineScopeImpl1 : CoroutineScope {
8+
override val coroutineContext: CoroutineContext
9+
get() = EmptyCoroutineContext
10+
}
11+
12+
internal class EmptyCoroutineScopeImpl2 : CoroutineScope {
13+
override val coroutineContext: CoroutineContext
14+
get() = EmptyCoroutineContext
15+
}
16+
17+
internal class EmptyCoroutineScopeImpl3 : CoroutineScope {
18+
override val coroutineContext: CoroutineContext
19+
get() = EmptyCoroutineContext
20+
}
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
kotlinx.coroutines.android.AndroidDispatcherFactory
1+
kotlinx.coroutines.android.AndroidDispatcherFactory

0 commit comments

Comments
 (0)