Fix classpath detection from classloader resources

This commit is contained in:
Ilya Chernikov
2019-06-01 13:51:33 +02:00
parent 27aa3a573e
commit 0a6ade88cc
3 changed files with 79 additions and 10 deletions
@@ -13,19 +13,50 @@ import java.io.FileOutputStream
import java.net.URLClassLoader import java.net.URLClassLoader
import java.util.jar.JarEntry import java.util.jar.JarEntry
import java.util.jar.JarOutputStream import java.util.jar.JarOutputStream
import java.util.jar.Manifest
import kotlin.script.experimental.jvm.util.classPathFromTypicalResourceUrls
import kotlin.script.experimental.jvm.util.classpathFromClassloader import kotlin.script.experimental.jvm.util.classpathFromClassloader
class ClassPathTest : TestCase() { class ClassPathTest : TestCase() {
lateinit var tempDir: File
override fun setUp() {
tempDir = createTempDir(ClassPathTest::class.simpleName!!)
super.setUp()
}
override fun tearDown() {
super.tearDown()
tempDir.deleteRecursively()
}
@Test @Test
fun testExtractFromFat() { fun testExtractFromFat() {
val collection = createTempFile("col", ".jar").apply { createCollectionJar(emulatedCollectionFiles, "BOOT-INF") } val collection = createTempFile("col", ".jar", directory = tempDir).apply { createCollectionJar(emulatedCollectionFiles, "BOOT-INF") }
val cl = URLClassLoader(arrayOf(collection.toURI().toURL()), null) val cl = URLClassLoader(arrayOf(collection.toURI().toURL()), null)
val cp = classpathFromClassloader(cl, true) val cp = classpathFromClassloader(cl, true)
Assert.assertTrue(cp != null && cp.isNotEmpty()) Assert.assertTrue(cp != null && cp.isNotEmpty())
testUnpackedCollection(cp!!, emulatedCollectionFiles) testUnpackedCollection(cp!!, emulatedCollectionFiles)
} }
@Test
fun testDetectClasspathFromResources() {
val root1 = createTempDir("root1", directory = tempDir)
val jar = createTempFile("jar1", ".jar", directory = tempDir).apply { createJarWithManifest() }
val cl = URLClassLoader(
(emulatedClasspath.map { File(root1, it).apply { mkdirs() }.toURI().toURL() }
+ jar.toURI().toURL()).toTypedArray(),
null
)
val cp = cl.classPathFromTypicalResourceUrls().toList()
Assert.assertTrue(cp.contains(jar.canonicalFile))
for (el in emulatedClasspath) {
Assert.assertTrue(cp.contains(File(root1, el).canonicalFile))
}
}
} }
private val emulatedCollectionFiles = arrayOf( private val emulatedCollectionFiles = arrayOf(
@@ -33,6 +64,11 @@ private val emulatedCollectionFiles = arrayOf(
"lib/c-d.jar" "lib/c-d.jar"
) )
private val emulatedClasspath = arrayOf(
"module1/classes/kotlin/main/",
"module2/classes/java/test/"
)
fun File.createCollectionJar(fileNames: Array<String>, infDirName: String) { fun File.createCollectionJar(fileNames: Array<String>, infDirName: String) {
FileOutputStream(this).use { fileStream -> FileOutputStream(this).use { fileStream ->
val jarStream = JarOutputStream(fileStream) val jarStream = JarOutputStream(fileStream)
@@ -59,4 +95,11 @@ fun testUnpackedCollection(classpath: List<File>, fileNames: Array<String>) {
Assert.assertTrue(cpClasses.size == 1) Assert.assertTrue(cpClasses.size == 1)
classes.checkFiles(cpClasses.first().parentFile) classes.checkFiles(cpClasses.first().parentFile)
jars.checkFiles(cpJars.first().parentFile.parentFile) jars.checkFiles(cpJars.first().parentFile.parentFile)
} }
fun File.createJarWithManifest() {
FileOutputStream(this).use { fileStream ->
val jarStream = JarOutputStream(fileStream, Manifest())
jarStream.finish()
}
}
@@ -100,7 +100,7 @@ internal fun URL.toFileOrNull() =
else File(file).canonicalFile else File(file).canonicalFile
} }
internal fun URL.toContainingFileOrNull(): File? = internal fun URL.toContainingJarOrNull(): File? =
if (protocol == "jar") { if (protocol == "jar") {
(openConnection() as? JarURLConnection)?.jarFileURL?.toFileOrNull() (openConnection() as? JarURLConnection)?.jarFileURL?.toFileOrNull()
} else toFileOrNull() } else null
@@ -8,11 +8,13 @@ package kotlin.script.experimental.jvm.util
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.net.JarURLConnection
import java.net.URL import java.net.URL
import java.net.URLClassLoader import java.net.URLClassLoader
import java.util.jar.JarInputStream import java.util.jar.JarInputStream
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.script.experimental.jvm.impl.toContainingFileOrNull import kotlin.script.experimental.jvm.impl.toContainingJarOrNull
import kotlin.script.experimental.jvm.impl.toFileOrNull
import kotlin.script.experimental.jvm.impl.tryGetResourcePathForClass import kotlin.script.experimental.jvm.impl.tryGetResourcePathForClass
import kotlin.script.experimental.jvm.impl.tryGetResourcePathForClassByName import kotlin.script.experimental.jvm.impl.tryGetResourcePathForClassByName
import kotlin.script.templates.standard.ScriptTemplateWithArgs import kotlin.script.templates.standard.ScriptTemplateWithArgs
@@ -67,7 +69,7 @@ fun classpathFromClassloader(currentClassLoader: ClassLoader, unpackJarCollectio
// if cache dir is specified, find all jar collections (spring boot fat jars and WARs so far, and unpack it accordingly // if cache dir is specified, find all jar collections (spring boot fat jars and WARs so far, and unpack it accordingly
val jarCollections = JAR_COLLECTIONS_KEY_PATHS.asSequence().flatMap { currentClassLoader.getResources(it).asSequence() } val jarCollections = JAR_COLLECTIONS_KEY_PATHS.asSequence().flatMap { currentClassLoader.getResources(it).asSequence() }
.mapNotNull { .mapNotNull {
it.toContainingFileOrNull()?.takeIf { file -> it.toContainingJarOrNull()?.takeIf { file ->
// additionally mark/check processed collection jars since unpacking is expensive // additionally mark/check processed collection jars since unpacking is expensive
file.extension in validJarCollectionFilesExtensions && processedJars.add(file) file.extension in validJarCollectionFilesExtensions && processedJars.add(file)
} }
@@ -80,7 +82,7 @@ fun classpathFromClassloader(currentClassLoader: ClassLoader, unpackJarCollectio
} }
else -> { else -> {
classLoader.classPathFromGetUrlsMethodOrNull() classLoader.classPathFromGetUrlsMethodOrNull()
?: classLoader.classPathFromManifestResourceUrls() ?: classLoader.classPathFromTypicalResourceUrls()
} }
} }
classPath classPath
@@ -88,7 +90,8 @@ fun classpathFromClassloader(currentClassLoader: ClassLoader, unpackJarCollectio
.toList().takeIf { it.isNotEmpty() } .toList().takeIf { it.isNotEmpty() }
} }
internal fun URL.toValidClasspathFileOrNull(): File? = toContainingFileOrNull()?.takeIf { it.isValidClasspathFile() } internal fun URL.toValidClasspathFileOrNull(): File? =
(toContainingJarOrNull() ?: toFileOrNull())?.takeIf { it.isValidClasspathFile() }
internal fun File.isValidClasspathFile(): Boolean = internal fun File.isValidClasspathFile(): Boolean =
isDirectory || (isFile && extension in validClasspathFilesExtensions) isDirectory || (isFile && extension in validClasspathFilesExtensions)
@@ -105,8 +108,31 @@ private fun ClassLoader.classPathFromGetUrlsMethodOrNull(): Sequence<File>? {
} }
} }
internal fun ClassLoader.classPathFromManifestResourceUrls(): Sequence<File> = internal fun ClassLoader.rawClassPathFromKeyResourcePath(keyResourcePath: String): Sequence<File> {
getResources(JAR_MANIFEST_RESOURCE_NAME).asSequence().distinct().mapNotNull { it.toValidClasspathFileOrNull() } var keyResourcePathDepth = -1
return getResources(keyResourcePath).asSequence().mapNotNull { url ->
if (url.protocol == "jar") {
(url.openConnection() as? JarURLConnection)?.jarFileURL?.toFileOrNull()
} else url.toFileOrNull()?.let { file ->
if (keyResourcePathDepth < 0) {
keyResourcePathDepth = keyResourcePath.trim('/').count { it == '/' }
}
var root = file
for (i in 1..keyResourcePathDepth) {
root = root.parentFile
}
root
}
}
}
fun ClassLoader.classPathFromTypicalResourceUrls(): Sequence<File> =
// roots without manifest cases are detected in some test scenarios
// manifests without containing directory entries are detected in some optimized jars, e.g. after proguard
// TODO: investigate whether getting resources with empty name works in all situations
(rawClassPathFromKeyResourcePath("") + rawClassPathFromKeyResourcePath(JAR_MANIFEST_RESOURCE_NAME))
.distinct()
.filter { it.isValidClasspathFile() }
private fun File.unpackJarCollection(rootTempDir: File?): Sequence<File> { private fun File.unpackJarCollection(rootTempDir: File?): Sequence<File> {
val targetDir = createTempDir(nameWithoutExtension, directory = rootTempDir) val targetDir = createTempDir(nameWithoutExtension, directory = rootTempDir)