[ENT-1731]: Record an event of loading and unloading CorDapps. (#818)

This commit is contained in:
Michele Sollecito 2018-05-10 22:01:43 +07:00 committed by GitHub
parent 32011e05d1
commit 590f626433
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 209 additions and 61 deletions

8
.idea/compiler.xml generated
View File

@ -48,14 +48,14 @@
<module name="client_test" target="1.8" />
<module name="confidential-identities_main" target="1.8" />
<module name="confidential-identities_test" target="1.8" />
<module name="corda-project-tools_main" target="1.8" />
<module name="corda-project-tools_test" target="1.8" />
<module name="contracts-states_integrationTest" target="1.8" />
<module name="contracts-states_main" target="1.8" />
<module name="contracts-states_test" target="1.8" />
<module name="corda-core_integrationTest" target="1.8" />
<module name="corda-core_smokeTest" target="1.8" />
<module name="corda-finance_integrationTest" target="1.8" />
<module name="corda-project-tools_main" target="1.8" />
<module name="corda-project-tools_test" target="1.8" />
<module name="corda-project_main" target="1.8" />
<module name="corda-project_test" target="1.8" />
<module name="cordapp-configuration_main" target="1.8" />
@ -95,6 +95,8 @@
<module name="finance_integrationTest" target="1.8" />
<module name="finance_main" target="1.8" />
<module name="finance_test" target="1.8" />
<module name="flow-hook_main" target="1.8" />
<module name="flow-hook_test" target="1.8" />
<module name="flows_integrationTest" target="1.8" />
<module name="flows_main" target="1.8" />
<module name="flows_test" target="1.8" />
@ -222,4 +224,4 @@
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_STRING" value="-parameters" />
</component>
</project>
</project>

View File

@ -51,4 +51,21 @@ interface Cordapp {
val customSchemas: Set<MappedSchema>
val jarPath: URL
val cordappClasses: List<String>
val info: Info
/**
* CorDapp's information, including vendor and version.
*
* @property shortName Cordapp's shortName
* @property vendor Cordapp's vendor
* @property version Cordapp's version
*/
@DoNotImplement
interface Info {
val shortName: String
val vendor: String
val version: String
fun hasUnknownFields(): Boolean
}
}

View File

@ -29,8 +29,9 @@ data class CordappImpl(
override val serializationWhitelists: List<SerializationWhitelist>,
override val serializationCustomSerializers: List<SerializationCustomSerializer<*, *>>,
override val customSchemas: Set<MappedSchema>,
override val jarPath: URL) : Cordapp {
override val name: String = jarPath.toPath().fileName.toString().removeSuffix(".jar")
override val jarPath: URL,
override val info: Cordapp.Info = CordappImpl.Info.UNKNOWN,
override val name: String = jarPath.toPath().fileName.toString().removeSuffix(".jar")) : Cordapp {
/**
* An exhaustive list of all classes relevant to the node within this CorDapp
@ -38,4 +39,16 @@ data class CordappImpl(
* TODO: Also add [SchedulableFlow] as a Cordapp class
*/
override val cordappClasses = ((rpcFlows + initiatedFlows + services + serializationWhitelists.map { javaClass }).map { it.name } + contractClassNames)
data class Info(override val shortName: String, override val vendor: String, override val version: String): Cordapp.Info {
companion object {
private const val UNKNOWN_VALUE = "Unknown"
val UNKNOWN = Info(UNKNOWN_VALUE, UNKNOWN_VALUE, UNKNOWN_VALUE)
}
override fun hasUnknownFields(): Boolean {
return setOf(shortName, vendor, version).any { it == UNKNOWN_VALUE }
}
}
}

View File

@ -20,7 +20,6 @@ import org.assertj.core.api.Assertions.assertThat
import java.time.Duration
class ScenarioState {
companion object {
val log = contextLogger()
}

View File

@ -81,6 +81,19 @@ jar {
exclude "META-INF/*.MF"
exclude "META-INF/LICENSE"
exclude "META-INF/NOTICE"
manifest {
attributes(
"Manifest-Version": "1.0",
"Name": "net/corda/finance",
"Specification-Title": description,
"Specification-Version": version,
"Specification-Vendor": "R3",
"Implementation-Title": "$group.$baseName",
"Implementation-Version": version,
"Implementation-Vendor": "R3"
)
}
}
publish {

View File

@ -24,4 +24,9 @@ data class VersionInfo(
/** The exact version control commit ID of the node build. */
val revision: String,
/** The node vendor */
val vendor: String)
val vendor: String) {
companion object {
val UNKNOWN = VersionInfo(1, "Unknown", "Unknown", "Unknown")
}
}

View File

@ -41,7 +41,7 @@ import java.util.concurrent.TimeUnit
open class EnterpriseNode(configuration: NodeConfiguration,
versionInfo: VersionInfo,
initialiseSerialization: Boolean = true,
cordappLoader: CordappLoader = makeCordappLoader(configuration)
cordappLoader: CordappLoader = makeCordappLoader(configuration, versionInfo)
) : Node(configuration, versionInfo, initialiseSerialization, cordappLoader) {
companion object {
private val logger by lazy { loggerFor<EnterpriseNode>() }

View File

@ -72,7 +72,7 @@ import kotlin.system.exitProcess
open class Node(configuration: NodeConfiguration,
versionInfo: VersionInfo,
private val initialiseSerialization: Boolean = true,
cordappLoader: CordappLoader = makeCordappLoader(configuration)
cordappLoader: CordappLoader = makeCordappLoader(configuration, versionInfo)
) : AbstractNode(configuration, createClock(configuration), versionInfo, cordappLoader) {
companion object {
private val staticLog = contextLogger()
@ -100,10 +100,10 @@ open class Node(configuration: NodeConfiguration,
const val scanPackagesSeparator = ","
@JvmStatic
protected fun makeCordappLoader(configuration: NodeConfiguration): CordappLoader {
protected fun makeCordappLoader(configuration: NodeConfiguration, versionInfo: VersionInfo): CordappLoader {
return System.getProperty(scanPackagesSystemProperty)?.let { scanPackages ->
CordappLoader.createDefaultWithTestPackages(configuration, scanPackages.split(scanPackagesSeparator))
} ?: CordappLoader.createDefault(configuration.baseDirectory)
CordappLoader.createDefaultWithTestPackages(configuration, scanPackages.split(scanPackagesSeparator), versionInfo)
} ?: CordappLoader.createDefault(configuration.baseDirectory, versionInfo)
}
// TODO Wire up maxMessageSize

View File

@ -12,13 +12,19 @@ package net.corda.node.internal
import com.jcabi.manifests.Manifests
import net.corda.core.crypto.Crypto
import net.corda.core.cordapp.Cordapp
import net.corda.core.internal.Emoji
import net.corda.core.internal.concurrent.thenMatch
import net.corda.core.internal.createDirectories
import net.corda.core.internal.div
import net.corda.core.internal.randomOrNull
import net.corda.core.utilities.loggerFor
import net.corda.node.*
import net.corda.node.CmdLineOptions
import net.corda.node.NodeArgsParser
import net.corda.node.NodeRegistrationOption
import net.corda.node.SerialFilter
import net.corda.node.VersionInfo
import net.corda.node.defaultSerialFilter
import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.config.NodeConfigurationImpl
import net.corda.node.services.config.shouldStartLocalShell
@ -28,8 +34,8 @@ import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
import net.corda.node.utilities.registration.NodeRegistrationHelper
import net.corda.nodeapi.internal.addShutdownHook
import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException
import net.corda.tools.shell.InteractiveShell
import net.corda.nodeapi.internal.persistence.oracleJdbcDriverSerialFilter
import net.corda.tools.shell.InteractiveShell
import org.fusesource.jansi.Ansi
import org.fusesource.jansi.AnsiConsole
import org.slf4j.bridge.SLF4JBridgeHandler
@ -153,7 +159,7 @@ open class NodeStartup(val args: Array<String>) {
return
}
val startedNode = node.start()
Node.printBasicNodeInfo("Loaded CorDapps", startedNode.services.cordappProvider.cordapps.joinToString { it.name })
logLoadedCorDapps(startedNode.services.cordappProvider.cordapps)
startedNode.internals.nodeReadyFuture.thenMatch({
val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0
val name = startedNode.info.legalIdentitiesAndCerts.first().name.organisation
@ -242,6 +248,17 @@ open class NodeStartup(val args: Array<String>) {
)
}
open protected fun logLoadedCorDapps(corDapps: List<Cordapp>) {
fun Cordapp.Info.description() = "$shortName version $version by $vendor"
Node.printBasicNodeInfo("Loaded ${corDapps.size} CorDapp(s)", corDapps.map { it.info }.joinToString(", ", transform = Cordapp.Info::description))
corDapps.map { it.info }.filter { it.hasUnknownFields() }.let { malformed ->
if (malformed.isNotEmpty()) {
logger.warn("Found ${malformed.size} CorDapp(s) with unknown information. They will be unable to run on Corda in the future.")
}
}
}
private fun enforceSingleNodeIsRunning(baseDirectory: Path) {
// Write out our process ID (which may or may not resemble a UNIX process id - to us it's just a string) to a
// file that we'll do our best to delete on exit. But if we don't, it'll be overwritten next time. If it already

View File

@ -14,15 +14,32 @@ import com.github.benmanes.caffeine.cache.Caffeine
import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult
import net.corda.core.cordapp.Cordapp
import net.corda.core.flows.*
import net.corda.core.internal.*
import net.corda.core.flows.ContractUpgradeFlow
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.SchedulableFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.flows.StartableByService
import net.corda.core.internal.VisibleForTesting
import net.corda.core.internal.copyTo
import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.internal.createDirectories
import net.corda.core.internal.div
import net.corda.core.internal.exists
import net.corda.core.internal.isRegularFile
import net.corda.core.internal.list
import net.corda.core.internal.objectOrNewInstance
import net.corda.core.internal.outputStream
import net.corda.core.internal.toPath
import net.corda.core.internal.toTypedArray
import net.corda.core.internal.walk
import net.corda.core.node.services.CordaService
import net.corda.core.schemas.MappedSchema
import net.corda.core.serialization.SerializationCustomSerializer
import net.corda.core.serialization.SerializationWhitelist
import net.corda.core.serialization.SerializeAsToken
import net.corda.core.utilities.contextLogger
import net.corda.node.VersionInfo
import net.corda.node.internal.classloading.requireAnnotation
import net.corda.node.services.config.NodeConfiguration
import net.corda.nodeapi.internal.coreContractClasses
@ -38,6 +55,7 @@ import java.nio.file.attribute.FileTime
import java.time.Instant
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.jar.JarInputStream
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry
import kotlin.reflect.KClass
@ -48,7 +66,7 @@ import kotlin.streams.toList
*
* @property cordappJarPaths The classpath of cordapp JARs
*/
class CordappLoader private constructor(private val cordappJarPaths: List<RestrictedURL>) {
class CordappLoader private constructor(private val cordappJarPaths: List<RestrictedURL>, versionInfo: VersionInfo) {
val cordapps: List<Cordapp> by lazy { loadCordapps() + coreCordapp }
val appClassLoader: ClassLoader = URLClassLoader(cordappJarPaths.stream().map { it.url }.toTypedArray(), javaClass.classLoader)
@ -62,6 +80,22 @@ class CordappLoader private constructor(private val cordappJarPaths: List<Restri
val cordappSchemas: Set<MappedSchema> get() = cordapps.flatMap { it.customSchemas }.toSet()
/** A Cordapp representing the core package which is not scanned automatically. */
@VisibleForTesting
internal val coreCordapp = CordappImpl(
contractClassNames = listOf(),
initiatedFlows = listOf(),
rpcFlows = coreRPCFlows,
serviceFlows = listOf(),
schedulableFlows = listOf(),
services = listOf(),
serializationWhitelists = listOf(),
serializationCustomSerializers = listOf(),
customSchemas = setOf(),
jarPath = ContractUpgradeFlow.javaClass.protectionDomain.codeSource.location, // Core JAR location
info = CordappImpl.Info("corda-core", versionInfo.vendor, versionInfo.releaseVersion)
)
companion object {
private val logger = contextLogger()
/**
@ -75,7 +109,7 @@ 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(getNodeCordappURLs(baseDir))
fun createDefault(baseDir: Path, versionInfo: VersionInfo = VersionInfo.UNKNOWN) = CordappLoader(getNodeCordappURLs(baseDir), versionInfo)
// Cache for CordappLoaders to avoid costly classpath scanning
private val cordappLoadersCache = Caffeine.newBuilder().softValues().build<List<RestrictedURL>, CordappLoader>()
@ -98,12 +132,12 @@ class CordappLoader private constructor(private val cordappJarPaths: List<Restri
* @param testPackages See [createWithTestPackages]
*/
@VisibleForTesting
fun createDefaultWithTestPackages(configuration: NodeConfiguration, testPackages: List<String>): CordappLoader {
fun createDefaultWithTestPackages(configuration: NodeConfiguration, testPackages: List<String>, versionInfo: VersionInfo = VersionInfo.UNKNOWN): CordappLoader {
if (!configuration.devMode) {
logger.warn("Package scanning should only occur in dev mode!")
}
val urls = getNodeCordappURLs(configuration.baseDirectory) + simplifyScanPackages(testPackages).flatMap(this::getPackageURLs)
return cordappLoadersCache.asMap().computeIfAbsent(urls, ::CordappLoader)
return cordappLoadersCache.asMap().computeIfAbsent(urls) { values -> CordappLoader(values, versionInfo) }
}
/**
@ -114,9 +148,9 @@ class CordappLoader private constructor(private val cordappJarPaths: List<Restri
* CorDapps.
*/
@VisibleForTesting
fun createWithTestPackages(testPackages: List<String>): CordappLoader {
fun createWithTestPackages(testPackages: List<String>, versionInfo: VersionInfo = VersionInfo.UNKNOWN): CordappLoader {
val urls = simplifyScanPackages(testPackages).flatMap(this::getPackageURLs)
return cordappLoadersCache.asMap().computeIfAbsent(urls, ::CordappLoader)
return cordappLoadersCache.asMap().computeIfAbsent(urls) { values -> CordappLoader(values, versionInfo) }
}
/**
@ -125,7 +159,7 @@ class CordappLoader private constructor(private val cordappJarPaths: List<Restri
* @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection
*/
@VisibleForTesting
fun createDevMode(scanJars: List<URL>) = CordappLoader(scanJars.map { RestrictedURL(it, null) })
fun createDevMode(scanJars: List<URL>, versionInfo: VersionInfo = VersionInfo.UNKNOWN) = CordappLoader(scanJars.map { RestrictedURL(it, null) }, versionInfo)
private fun getPackageURLs(scanPackage: String): List<RestrictedURL> {
val resource = scanPackage.replace('.', '/')
@ -148,20 +182,24 @@ class CordappLoader private constructor(private val cordappJarPaths: List<Restri
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"
val uuid = UUID.randomUUID()
val cordappJar = cordappDir / "$scanPackage-$uuid.jar"
logger.info("Generating a test-only CorDapp of classes discovered for package $scanPackage in $url: $cordappJar")
JarOutputStream(cordappJar.outputStream()).use { jos ->
val scanDir = url.toPath()
scanDir.walk { 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()) {
it.copyTo(jos)
val manifest = createTestManifest(resource, scanPackage, uuid)
val scanDir = url.toPath()
JarOutputStream(cordappJar.outputStream(), manifest).use { jos ->
scanDir.walk {
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()) {
it.copyTo(jos)
}
jos.closeEntry()
}
jos.closeEntry()
} }
}
}
cordappJar
}
@ -183,25 +221,13 @@ class CordappLoader private constructor(private val cordappJarPaths: List<Restri
ContractUpgradeFlow.Initiate::class.java,
ContractUpgradeFlow.Authorise::class.java,
ContractUpgradeFlow.Deauthorise::class.java)
/** A Cordapp representing the core package which is not scanned automatically. */
@VisibleForTesting
internal val coreCordapp = CordappImpl(
contractClassNames = listOf(),
initiatedFlows = listOf(),
rpcFlows = coreRPCFlows,
serviceFlows = listOf(),
schedulableFlows = listOf(),
services = listOf(),
serializationWhitelists = listOf(),
serializationCustomSerializers = listOf(),
customSchemas = setOf(),
jarPath = ContractUpgradeFlow.javaClass.protectionDomain.codeSource.location // Core JAR location
)
}
private fun loadCordapps(): List<Cordapp> {
return cordappJarPaths.map {
val url = it.url
val name = url.toPath().fileName.toString().removeSuffix(".jar")
val info = url.openStream().let(::JarInputStream).use { it.manifest }.toCordappInfo(name)
val scanResult = scanCordapp(it)
CordappImpl(findContractClassNames(scanResult),
findInitiatedFlows(scanResult),
@ -212,7 +238,9 @@ class CordappLoader private constructor(private val cordappJarPaths: List<Restri
findPlugins(it),
findSerializers(scanResult),
findCustomSchemas(scanResult),
it.url)
url,
info,
name)
}
}
@ -303,7 +331,7 @@ class CordappLoader private constructor(private val cordappJarPaths: List<Restri
/** @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 + '.' } ?: ""
val qualifiedNamePrefix: String get() = rootPackageName?.let { "$it." } ?: ""
}
private inner class RestrictedScanResult(private val scanResult: ScanResult, private val qualifiedNamePrefix: String) {

View File

@ -0,0 +1,56 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package net.corda.node.internal.cordapp
import net.corda.core.cordapp.Cordapp
import net.corda.core.internal.cordapp.CordappImpl
import java.util.*
import java.util.jar.Attributes
import java.util.jar.Manifest
internal fun createTestManifest(name: String, title: String, jarUUID: UUID): Manifest {
val manifest = Manifest()
val version = "test-$jarUUID"
val vendor = "R3"
// Mandatory manifest attribute. If not present, all other entries are silently skipped.
manifest.mainAttributes[Attributes.Name.MANIFEST_VERSION] = "1.0"
manifest["Name"] = name
manifest["Specification-Title"] = title
manifest["Specification-Version"] = version
manifest["Specification-Vendor"] = vendor
manifest["Implementation-Title"] = title
manifest["Implementation-Version"] = version
manifest["Implementation-Vendor"] = vendor
return manifest
}
internal operator fun Manifest.set(key: String, value: String) {
mainAttributes.putValue(key, value)
}
internal fun Manifest?.toCordappInfo(defaultShortName: String): Cordapp.Info {
var unknown = CordappImpl.Info.UNKNOWN
(this?.mainAttributes?.getValue("Name") ?: defaultShortName).let { shortName ->
unknown = unknown.copy(shortName = shortName)
}
this?.mainAttributes?.getValue("Implementation-Vendor")?.let { vendor ->
unknown = unknown.copy(vendor = vendor)
}
this?.mainAttributes?.getValue("Implementation-Version")?.let { version ->
unknown = unknown.copy(version = version)
}
return unknown
}

View File

@ -64,7 +64,7 @@ class NodeTest {
fun `generateAndSaveNodeInfo works`() {
val nodeAddress = NetworkHostAndPort("0.1.2.3", 456)
val nodeName = CordaX500Name("Manx Blockchain Corp", "Douglas", "IM")
val platformVersion = 789
val info = VersionInfo(789, "3.0", "SNAPSHOT", "R3")
val dataSourceProperties = makeTestDataSourceProperties()
val databaseConfig = DatabaseConfig()
val configuration = rigorousMock<AbstractNodeConfiguration>().also {
@ -79,10 +79,8 @@ class NodeTest {
doReturn("tsp").whenever(it).trustStorePassword
doReturn("ksp").whenever(it).keyStorePassword
}
configureDatabase(dataSourceProperties, databaseConfig, rigorousMock()).use { database ->
val node = Node(configuration, rigorousMock<VersionInfo>().also {
doReturn(platformVersion).whenever(it).platformVersion
}, initialiseSerialization = false)
configureDatabase(dataSourceProperties, databaseConfig, rigorousMock()).use { _ ->
val node = Node(configuration, info, initialiseSerialization = false)
assertEquals(node.generateNodeInfo(), node.generateNodeInfo()) // Node info doesn't change (including the serial)
}
}

View File

@ -51,7 +51,7 @@ class CordappLoaderTest {
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).containsOnly(CordappLoader.coreCordapp)
assertThat(loader.cordapps).containsOnly(loader.coreCordapp)
}
@Test
@ -62,7 +62,7 @@ class CordappLoaderTest {
val actual = loader.cordapps.toTypedArray()
assertThat(actual).hasSize(2)
val actualCordapp = actual.single { it != CordappLoader.coreCordapp }
val actualCordapp = actual.single { it != loader.coreCordapp }
assertThat(actualCordapp.contractClassNames).isEqualTo(listOf(isolatedContractId))
assertThat(actualCordapp.initiatedFlows.single().name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Acceptor")
assertThat(actualCordapp.rpcFlows).isEmpty()

View File

@ -41,7 +41,7 @@ class AttachmentDemoTest : IntegrationTest() {
@Test
fun `attachment demo using a 10MB zip file`() {
val numOfExpectedBytes = 10_000_000
driver(DriverParameters(isDebug = true, portAllocation = PortAllocation.Incremental(20000))) {
driver(DriverParameters(isDebug = true, portAllocation = PortAllocation.Incremental(20000), startNodesInProcess = true)) {
val demoUser = listOf(User("demo", "demo", setOf(
startFlow<AttachmentDemoFlow>(),
invokeRpc(CordaRPCOps::attachmentExists),