<lambda>null1 package kotlinx.coroutines.internal
2
3 import java.io.*
4 import java.net.*
5 import java.util.*
6 import java.util.jar.*
7 import java.util.zip.*
8 import kotlin.collections.ArrayList
9
10 /**
11 * Don't use JvmField here to enable R8 optimizations via "assumenosideeffects"
12 */
13 internal val ANDROID_DETECTED = runCatching { Class.forName("android.os.Build") }.isSuccess
14
15 /**
16 * A simplified version of [ServiceLoader].
17 * FastServiceLoader locates and instantiates all service providers named in configuration
18 * files placed in the resource directory <tt>META-INF/services</tt>.
19 *
20 * The main difference between this class and classic service loader is in skipping
21 * verification JARs. A verification requires reading the whole JAR (and it causes problems and ANRs on Android devices)
22 * and prevents only trivial checksum issues. See #878.
23 *
24 * If any error occurs during loading, it fallbacks to [ServiceLoader], mostly to prevent R8 issues.
25 */
26 internal object FastServiceLoader {
27 private const val PREFIX: String = "META-INF/services/"
28
29 /**
30 * This method attempts to load [MainDispatcherFactory] in Android-friendly way.
31 *
32 * If we are not on Android, this method fallbacks to a regular service loading,
33 * else we attempt to do `Class.forName` lookup for
34 * `AndroidDispatcherFactory` and `TestMainDispatcherFactory`.
35 * If lookups are successful, we return resultinAg instances because we know that
36 * `MainDispatcherFactory` API is internal and this is the only possible classes of `MainDispatcherFactory` Service on Android.
37 *
38 * Such intricate dance is required to avoid calls to `ServiceLoader.load` for multiple reasons:
39 * 1) It eliminates disk lookup on potentially slow devices on the Main thread.
40 * 2) Various Android toolchain versions by various vendors don't tend to handle ServiceLoader calls properly.
41 * Sometimes META-INF is removed from the resulting APK, sometimes class names are mangled, etc.
42 * While it is not the problem of `kotlinx.coroutines`, it significantly worsens user experience, thus we are workarounding it.
43 * Examples of such issues are #932, #1072, #1557, #1567
44 *
45 * We also use SL for [CoroutineExceptionHandler], but we do not experience the same problems and CEH is a public API
46 * that may already be injected vis SL, so we are not using the same technique for it.
47 */
loadMainDispatcherFactorynull48 internal fun loadMainDispatcherFactory(): List<MainDispatcherFactory> {
49 val clz = MainDispatcherFactory::class.java
50 if (!ANDROID_DETECTED) {
51 return load(clz, clz.classLoader)
52 }
53
54 return try {
55 val result = ArrayList<MainDispatcherFactory>(2)
56 createInstanceOf(clz, "kotlinx.coroutines.android.AndroidDispatcherFactory")?.apply { result.add(this) }
57 createInstanceOf(clz, "kotlinx.coroutines.test.internal.TestMainDispatcherFactory")?.apply { result.add(this) }
58 result
59 } catch (e: Throwable) {
60 // Fallback to the regular SL in case of any unexpected exception
61 load(clz, clz.classLoader)
62 }
63 }
64
65 /*
66 * This method is inline to have a direct Class.forName("string literal") in the byte code to avoid weird interactions with ProGuard/R8.
67 */
68 @Suppress("NOTHING_TO_INLINE")
createInstanceOfnull69 private inline fun createInstanceOf(
70 baseClass: Class<MainDispatcherFactory>,
71 serviceClass: String
72 ): MainDispatcherFactory? {
73 return try {
74 val clz = Class.forName(serviceClass, true, baseClass.classLoader)
75 baseClass.cast(clz.getDeclaredConstructor().newInstance())
76 } catch (e: ClassNotFoundException) { // Do not fail if TestMainDispatcherFactory is not found
77 null
78 }
79 }
80
loadnull81 private fun <S> load(service: Class<S>, loader: ClassLoader): List<S> {
82 return try {
83 loadProviders(service, loader)
84 } catch (e: Throwable) {
85 // Fallback to default service loader
86 ServiceLoader.load(service, loader).toList()
87 }
88 }
89
90 // Visible for tests
loadProvidersnull91 internal fun <S> loadProviders(service: Class<S>, loader: ClassLoader): List<S> {
92 val fullServiceName = PREFIX + service.name
93 // Filter out situations when both JAR and regular files are in the classpath (e.g. IDEA)
94 val urls = loader.getResources(fullServiceName)
95 val providers = urls.toList().flatMap { parse(it) }.toSet()
96 require(providers.isNotEmpty()) { "No providers were loaded with FastServiceLoader" }
97 return providers.map { getProviderInstance(it, loader, service) }
98 }
99
getProviderInstancenull100 private fun <S> getProviderInstance(name: String, loader: ClassLoader, service: Class<S>): S {
101 val clazz = Class.forName(name, false, loader)
102 require(service.isAssignableFrom(clazz)) { "Expected service of class $service, but found $clazz" }
103 return service.cast(clazz.getDeclaredConstructor().newInstance())
104 }
105
parsenull106 private fun parse(url: URL): List<String> {
107 val path = url.toString()
108 // Fast-path for JARs
109 if (path.startsWith("jar")) {
110 val pathToJar = path.substringAfter("jar:file:").substringBefore('!')
111 val entry = path.substringAfter("!/")
112 // mind the verify = false flag!
113 (JarFile(pathToJar, false)).use { file ->
114 BufferedReader(InputStreamReader(file.getInputStream(ZipEntry(entry)), "UTF-8")).use { r ->
115 return parseFile(r)
116 }
117 }
118 }
119 // Regular path for everything else
120 return BufferedReader(InputStreamReader(url.openStream())).use { reader ->
121 parseFile(reader)
122 }
123 }
124
125 // JarFile does no implement Closesable on Java 1.6
usenull126 private inline fun <R> JarFile.use(block: (JarFile) -> R): R {
127 var cause: Throwable? = null
128 try {
129 return block(this)
130 } catch (e: Throwable) {
131 cause = e
132 throw e
133 } finally {
134 try {
135 close()
136 } catch (closeException: Throwable) {
137 if (cause === null) throw closeException
138 cause.addSuppressed(closeException)
139 throw cause
140 }
141 }
142 }
143
parseFilenull144 private fun parseFile(r: BufferedReader): List<String> {
145 val names = mutableSetOf<String>()
146 while (true) {
147 val line = r.readLine() ?: break
148 val serviceName = line.substringBefore("#").trim()
149 require(serviceName.all { it == '.' || Character.isJavaIdentifierPart(it) }) { "Illegal service provider class name: $serviceName" }
150 if (serviceName.isNotEmpty()) {
151 names.add(serviceName)
152 }
153 }
154 return names.toList()
155 }
156 }
157