mirror of
https://github.com/corda/corda.git
synced 2025-01-31 16:35:43 +00:00
CORDA-1385 - Ignore duplicate packages and sub-packages in driver extraCordappPackagesToScan (#3068)
Otherwise duplicate test CorDapps are loaded into the node (cherry picked from commit 9ffb43f)
This commit is contained in:
parent
baf5c97e0c
commit
05671af82a
@ -37,13 +37,13 @@ class CordappScanningDriverTest {
|
||||
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class ReceiveFlow(val otherParty: Party) : FlowLogic<String>() {
|
||||
class ReceiveFlow(private val otherParty: Party) : FlowLogic<String>() {
|
||||
@Suspendable
|
||||
override fun call(): String = initiateFlow(otherParty).receive<String>().unwrap { it }
|
||||
}
|
||||
|
||||
@InitiatedBy(ReceiveFlow::class)
|
||||
open class SendClassFlow(val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
open class SendClassFlow(private val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() = otherPartySession.send(javaClass.name)
|
||||
}
|
||||
|
@ -85,8 +85,10 @@ open class Node(configuration: NodeConfiguration,
|
||||
}
|
||||
|
||||
private val sameVmNodeCounter = AtomicInteger()
|
||||
val scanPackagesSystemProperty = "net.corda.node.cordapp.scan.packages"
|
||||
val scanPackagesSeparator = ","
|
||||
|
||||
const val scanPackagesSystemProperty = "net.corda.node.cordapp.scan.packages"
|
||||
const val scanPackagesSeparator = ","
|
||||
|
||||
private fun makeCordappLoader(configuration: NodeConfiguration): CordappLoader {
|
||||
return System.getProperty(scanPackagesSystemProperty)?.let { scanPackages ->
|
||||
CordappLoader.createDefaultWithTestPackages(configuration, scanPackages.split(scanPackagesSeparator))
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.node.internal.cordapp
|
||||
|
||||
import com.google.common.cache.CacheBuilder
|
||||
import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
|
||||
import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult
|
||||
import net.corda.core.contracts.Contract
|
||||
@ -19,18 +20,17 @@ import net.corda.node.internal.classloading.requireAnnotation
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.nodeapi.internal.serialization.DefaultWhitelist
|
||||
import org.apache.commons.collections4.map.LRUMap
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.lang.reflect.Modifier
|
||||
import java.net.JarURLConnection
|
||||
import java.net.URI
|
||||
import java.net.URL
|
||||
import java.net.URLClassLoader
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.attribute.FileTime
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.jar.JarOutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import kotlin.reflect.KClass
|
||||
@ -69,10 +69,21 @@ class CordappLoader private constructor(private val cordappJarPaths: List<Restri
|
||||
* @param baseDir The directory that this node is running in. Will use this to resolve the cordapps directory
|
||||
* for classpath scanning.
|
||||
*/
|
||||
fun createDefault(baseDir: Path) = CordappLoader(getCordappsInDirectory(getCordappsPath(baseDir)))
|
||||
fun createDefault(baseDir: Path) = CordappLoader(getNodeCordappURLs(baseDir))
|
||||
|
||||
// Cache for CordappLoaders to avoid costly classpath scanning
|
||||
private val cordappLoadersCache = LRUMap<List<*>, CordappLoader>(1000)
|
||||
private val cordappLoadersCache = CacheBuilder.newBuilder().softValues().build<List<RestrictedURL>, CordappLoader>()
|
||||
private val generatedCordapps = ConcurrentHashMap<URL, Path>()
|
||||
|
||||
private fun simplifyScanPackages(scanPackages: List<String>): List<String> {
|
||||
return scanPackages.sorted().fold(emptyList()) { listSoFar, packageName ->
|
||||
when {
|
||||
listSoFar.isEmpty() -> listOf(packageName)
|
||||
packageName.startsWith(listSoFar.last()) -> listSoFar // Squash ["com.foo", "com.foo.bar"] into just ["com.foo"]
|
||||
else -> listSoFar + packageName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a dev mode CordappLoader for test environments that creates and loads cordapps from the classpath
|
||||
@ -86,8 +97,8 @@ class CordappLoader private constructor(private val cordappJarPaths: List<Restri
|
||||
if (!configuration.devMode) {
|
||||
logger.warn("Package scanning should only occur in dev mode!")
|
||||
}
|
||||
val paths = getCordappsInDirectory(getCordappsPath(configuration.baseDirectory)) + testPackages.flatMap(this::createScanPackage)
|
||||
return cordappLoadersCache.computeIfAbsent(paths, { CordappLoader(paths) })
|
||||
val urls = getNodeCordappURLs(configuration.baseDirectory) + simplifyScanPackages(testPackages).flatMap(this::getPackageURLs)
|
||||
return cordappLoadersCache.asMap().computeIfAbsent(urls, ::CordappLoader)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -99,7 +110,8 @@ class CordappLoader private constructor(private val cordappJarPaths: List<Restri
|
||||
*/
|
||||
@VisibleForTesting
|
||||
fun createWithTestPackages(testPackages: List<String>): CordappLoader {
|
||||
return cordappLoadersCache.computeIfAbsent(testPackages, { CordappLoader(testPackages.flatMap(this::createScanPackage)) })
|
||||
val urls = simplifyScanPackages(testPackages).flatMap(this::getPackageURLs)
|
||||
return cordappLoadersCache.asMap().computeIfAbsent(urls, ::CordappLoader)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -110,64 +122,57 @@ class CordappLoader private constructor(private val cordappJarPaths: List<Restri
|
||||
@VisibleForTesting
|
||||
fun createDevMode(scanJars: List<URL>) = CordappLoader(scanJars.map { RestrictedURL(it, null) })
|
||||
|
||||
private fun getCordappsPath(baseDir: Path): Path = baseDir / CORDAPPS_DIR_NAME
|
||||
|
||||
private fun createScanPackage(scanPackage: String): List<RestrictedURL> {
|
||||
private fun getPackageURLs(scanPackage: String): List<RestrictedURL> {
|
||||
val resource = scanPackage.replace('.', '/')
|
||||
return this::class.java.classLoader.getResources(resource)
|
||||
.asSequence()
|
||||
.map { path ->
|
||||
if (path.protocol == "jar") {
|
||||
.map { url ->
|
||||
if (url.protocol == "jar") {
|
||||
// When running tests from gradle this may be a corda module jar, so restrict to scanPackage:
|
||||
RestrictedURL((path.openConnection() as JarURLConnection).jarFileURL, scanPackage)
|
||||
RestrictedURL((url.openConnection() as JarURLConnection).jarFileURL, scanPackage)
|
||||
} else {
|
||||
// No need to restrict as createDevCordappJar has already done that:
|
||||
RestrictedURL(createDevCordappJar(scanPackage, path, resource).toURL(), null)
|
||||
RestrictedURL(createDevCordappJar(scanPackage, url, resource).toUri().toURL(), null)
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
/** Takes a package of classes and creates a JAR from them - only use in tests. */
|
||||
private fun createDevCordappJar(scanPackage: String, path: URL, jarPackageName: String): URI {
|
||||
if (!generatedCordapps.contains(path)) {
|
||||
val cordappDir = File("build/tmp/generated-test-cordapps")
|
||||
cordappDir.mkdirs()
|
||||
val cordappJAR = File(cordappDir, "$scanPackage-${UUID.randomUUID()}.jar")
|
||||
logger.info("Generating a test-only cordapp of classes discovered in $scanPackage at $cordappJAR")
|
||||
FileOutputStream(cordappJAR).use {
|
||||
JarOutputStream(it).use { jos ->
|
||||
val scanDir = File(path.toURI())
|
||||
scanDir.walkTopDown().forEach {
|
||||
val entryPath = jarPackageName + "/" + scanDir.toPath().relativize(it.toPath()).toString().replace('\\', '/')
|
||||
val time = FileTime.from(Instant.EPOCH)
|
||||
val entry = ZipEntry(entryPath).setCreationTime(time).setLastAccessTime(time).setLastModifiedTime(time)
|
||||
jos.putNextEntry(entry)
|
||||
if (it.isFile) {
|
||||
Files.copy(it.toPath(), jos)
|
||||
}
|
||||
jos.closeEntry()
|
||||
private fun createDevCordappJar(scanPackage: String, url: URL, resource: String): Path {
|
||||
return generatedCordapps.computeIfAbsent(url) {
|
||||
// TODO Using the driver in out-of-process mode causes each node to have their own copy of the same dev CorDapps
|
||||
val cordappDir = (Paths.get("build") / "tmp" / "generated-test-cordapps").createDirectories()
|
||||
val cordappJar = cordappDir / "$scanPackage-${UUID.randomUUID()}.jar"
|
||||
logger.info("Generating a test-only CorDapp of classes discovered for package $scanPackage in $url: $cordappJar")
|
||||
JarOutputStream(Files.newOutputStream(cordappJar)).use { jos ->
|
||||
val scanDir = Paths.get(url.toURI())
|
||||
Files.walk(scanDir).use { it.forEach {
|
||||
val entryPath = "$resource/${scanDir.relativize(it).toString().replace('\\', '/')}"
|
||||
val time = FileTime.from(Instant.EPOCH)
|
||||
val entry = ZipEntry(entryPath).setCreationTime(time).setLastAccessTime(time).setLastModifiedTime(time)
|
||||
jos.putNextEntry(entry)
|
||||
if (it.isRegularFile()) {
|
||||
Files.copy(it, jos)
|
||||
}
|
||||
}
|
||||
jos.closeEntry()
|
||||
} }
|
||||
}
|
||||
generatedCordapps[path] = cordappJAR.toURI()
|
||||
cordappJar
|
||||
}
|
||||
|
||||
return generatedCordapps[path]!!
|
||||
}
|
||||
|
||||
private fun getCordappsInDirectory(cordappsDir: Path): List<RestrictedURL> {
|
||||
private fun getNodeCordappURLs(baseDir: Path): List<RestrictedURL> {
|
||||
val cordappsDir = baseDir / CORDAPPS_DIR_NAME
|
||||
return if (!cordappsDir.exists()) {
|
||||
emptyList()
|
||||
} else {
|
||||
cordappsDir.list {
|
||||
it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.map { RestrictedURL(it.toUri().toURL(), null) }.toList()
|
||||
it.filter { it.toString().endsWith(".jar") }.map { RestrictedURL(it.toUri().toURL(), null) }.toList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val generatedCordapps = mutableMapOf<URL, URI>()
|
||||
|
||||
/** A list of the core RPC flows present in Corda */
|
||||
private val coreRPCFlows = listOf(
|
||||
ContractUpgradeFlow.Initiate::class.java,
|
||||
@ -267,7 +272,7 @@ class CordappLoader private constructor(private val cordappJarPaths: List<Restri
|
||||
|
||||
private val cachedScanResult = LRUMap<RestrictedURL, RestrictedScanResult>(1000)
|
||||
private fun scanCordapp(cordappJarPath: RestrictedURL): RestrictedScanResult {
|
||||
logger.info("Scanning CorDapp in $cordappJarPath")
|
||||
logger.info("Scanning CorDapp in ${cordappJarPath.url}")
|
||||
return cachedScanResult.computeIfAbsent(cordappJarPath, {
|
||||
RestrictedScanResult(FastClasspathScanner().addClassLoader(appClassLoader).overrideClasspath(cordappJarPath.url).scan(), cordappJarPath.qualifiedNamePrefix)
|
||||
})
|
||||
@ -297,10 +302,9 @@ class CordappLoader private constructor(private val cordappJarPaths: List<Restri
|
||||
}
|
||||
}
|
||||
|
||||
/** @param rootPackageName only this package and subpackages may be extracted from [url], or null to allow all packages. */
|
||||
private class RestrictedURL(val url: URL, rootPackageName: String?) {
|
||||
val qualifiedNamePrefix = rootPackageName?.let { it + '.' } ?: ""
|
||||
override fun toString() = url.toString()
|
||||
/** @property rootPackageName only this package and subpackages may be extracted from [url], or null to allow all packages. */
|
||||
private data class RestrictedURL(val url: URL, val rootPackageName: String?) {
|
||||
val qualifiedNamePrefix: String get() = rootPackageName?.let { it + '.' } ?: ""
|
||||
}
|
||||
|
||||
private inner class RestrictedScanResult(private val scanResult: ScanResult, private val qualifiedNamePrefix: String) {
|
||||
|
@ -9,45 +9,39 @@ import java.nio.file.Paths
|
||||
@InitiatingFlow
|
||||
class DummyFlow : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
}
|
||||
override fun call() = Unit
|
||||
}
|
||||
|
||||
@InitiatedBy(DummyFlow::class)
|
||||
class LoaderTestFlow(unusedSession: FlowSession) : FlowLogic<Unit>() {
|
||||
class LoaderTestFlow(@Suppress("UNUSED_PARAMETER") unusedSession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
}
|
||||
override fun call() = Unit
|
||||
}
|
||||
|
||||
@SchedulableFlow
|
||||
class DummySchedulableFlow : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
}
|
||||
override fun call() = Unit
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
class DummyRPCFlow : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
}
|
||||
override fun call() = Unit
|
||||
}
|
||||
|
||||
class CordappLoaderTest {
|
||||
private companion object {
|
||||
val testScanPackages = listOf("net.corda.node.internal.cordapp")
|
||||
val isolatedContractId = "net.corda.finance.contracts.isolated.AnotherDummyContract"
|
||||
val isolatedFlowName = "net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator"
|
||||
const val testScanPackage = "net.corda.node.internal.cordapp"
|
||||
const val isolatedContractId = "net.corda.finance.contracts.isolated.AnotherDummyContract"
|
||||
const val isolatedFlowName = "net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test that classes that aren't in cordapps aren't loaded`() {
|
||||
// Basedir will not be a corda node directory so the dummy flow shouldn't be recognised as a part of a cordapp
|
||||
val loader = CordappLoader.createDefault(Paths.get("."))
|
||||
assertThat(loader.cordapps)
|
||||
.hasSize(1)
|
||||
.contains(CordappLoader.coreCordapp)
|
||||
assertThat(loader.cordapps).containsOnly(CordappLoader.coreCordapp)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -71,7 +65,7 @@ class CordappLoaderTest {
|
||||
|
||||
@Test
|
||||
fun `flows are loaded by loader`() {
|
||||
val loader = CordappLoader.createWithTestPackages(testScanPackages)
|
||||
val loader = CordappLoader.createWithTestPackages(listOf(testScanPackage))
|
||||
|
||||
val actual = loader.cordapps.toTypedArray()
|
||||
// One core cordapp, one cordapp from this source tree, and two others due to identically named locations
|
||||
@ -85,6 +79,20 @@ class CordappLoaderTest {
|
||||
assertThat(actualCordapp.schedulableFlows).first().hasSameClassAs(DummySchedulableFlow::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `duplicate packages are ignored`() {
|
||||
val loader = CordappLoader.createWithTestPackages(listOf(testScanPackage, testScanPackage))
|
||||
val cordapps = loader.cordapps.filter { LoaderTestFlow::class.java in it.initiatedFlows }
|
||||
assertThat(cordapps).hasSize(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sub-packages are ignored`() {
|
||||
val loader = CordappLoader.createWithTestPackages(listOf("net.corda", testScanPackage))
|
||||
val cordapps = loader.cordapps.filter { LoaderTestFlow::class.java in it.initiatedFlows }
|
||||
assertThat(cordapps).hasSize(1)
|
||||
}
|
||||
|
||||
// This test exists because the appClassLoader is used by serialisation and we need to ensure it is the classloader
|
||||
// being used internally. Later iterations will use a classloader per cordapp and this test can be retired.
|
||||
@Test
|
||||
|
Loading…
x
Reference in New Issue
Block a user