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