diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt index 36926ac661..6d1d1f41a0 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt @@ -10,29 +10,36 @@ package net.corda.client.rpc +import net.corda.client.rpc.internal.createCordaRPCClientWithSslAndClassLoader import net.corda.core.context.* +import net.corda.core.contracts.FungibleAsset import net.corda.core.crypto.random63BitValue import net.corda.core.identity.Party import net.corda.core.internal.concurrent.flatMap -import net.corda.core.internal.packageName +import net.corda.core.internal.location +import net.corda.core.internal.toPath import net.corda.core.messaging.* +import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.finance.DOLLARS +import net.corda.finance.POUNDS import net.corda.finance.USD +import net.corda.finance.contracts.asset.Cash import net.corda.finance.contracts.getCashBalance import net.corda.finance.contracts.getCashBalances import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow -import net.corda.finance.schemas.CashSchemaV1 import net.corda.node.internal.Node import net.corda.node.internal.StartedNode import net.corda.node.services.Permissions.Companion.all +import net.corda.testing.common.internal.checkNotOnClasspath import net.corda.testing.core.* import net.corda.testing.node.User import net.corda.testing.internal.IntegrationTestSchemas import net.corda.testing.internal.toDatabaseSchemaName import net.corda.testing.node.internal.NodeBasedTest +import net.corda.testing.node.internal.ProcessUtilities import org.apache.activemq.artemis.api.core.ActiveMQSecurityException import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType @@ -41,6 +48,10 @@ import org.junit.Before import org.junit.ClassRule import org.junit.Test import rx.subjects.PublishSubject +import java.io.File.pathSeparator +import java.net.URLClassLoader +import java.nio.file.Paths +import java.util.* import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService @@ -49,9 +60,11 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance.contracts", CashSchemaV1::class.packageName)) { - private val rpcUser = User("user1", "test", permissions = setOf(all()) - ) +class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance")) { + companion object { + val rpcUser = User("user1", "test", permissions = setOf(all())) + } + private lateinit var node: StartedNode private lateinit var identity: Party private lateinit var client: CordaRPCClient @@ -70,7 +83,7 @@ class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance.contracts", C override fun setUp() { super.setUp() node = startNode(ALICE_NAME, rpcUsers = listOf(rpcUser)) - client = CordaRPCClient(node.internals.configuration.rpcOptions.address!!, CordaRPCClientConfiguration.DEFAULT.copy( + client = CordaRPCClient(node.internals.configuration.rpcOptions.address, CordaRPCClientConfiguration.DEFAULT.copy( maxReconnectAttempts = 5 )) identity = node.info.identityFromX500Name(ALICE_NAME) @@ -102,7 +115,6 @@ class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance.contracts", C @Test fun `shutdown command stops the node`() { - val nodeIsShut: PublishSubject = PublishSubject.create() val latch = CountDownLatch(1) var successful = false @@ -149,7 +161,6 @@ class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance.contracts", C } private class CloseableExecutor(private val delegate: ScheduledExecutorService) : AutoCloseable, ScheduledExecutorService by delegate { - override fun close() { delegate.shutdown() } @@ -228,19 +239,70 @@ class CordaRPCClientTest : NodeBasedTest(listOf("net.corda.finance.contracts", C ) } } -} -private fun checkShellNotification(info: StateMachineInfo) { - val context = info.invocationContext - assertThat(context.origin).isInstanceOf(InvocationOrigin.Shell::class.java) -} + // WireTransaction stores its components as blobs which are deserialised in its constructor. This test makes sure + // the extra class loader given to the CordaRPCClient is used in this deserialisation, as otherwise any WireTransaction + // containing Cash.State objects are not receivable by the client. + // + // We run the client in a separate process, without the finance module on its system classpath to ensure that the + // additional class loader that we give it is used. Cash.State objects are used as they can't be synthesised fully + // by the carpenter, and thus avoiding any false-positive results. + @Test + fun `additional class loader used by WireTransaction when it deserialises its components`() { + val financeLocation = Cash::class.java.location.toPath().toString() + val classpathWithoutFinance = ProcessUtilities.defaultClassPath + .split(pathSeparator) + .filter { financeLocation !in it } + .joinToString(pathSeparator) -private fun checkRpcNotification(info: StateMachineInfo, rpcUsername: String, historicalIds: MutableSet, externalTrace: Trace?, impersonatedActor: Actor?) { - val context = info.invocationContext - assertThat(context.origin).isInstanceOf(InvocationOrigin.RPC::class.java) - assertThat(context.externalTrace).isEqualTo(externalTrace) - assertThat(context.impersonatedActor).isEqualTo(impersonatedActor) - assertThat(context.actor?.id?.value).isEqualTo(rpcUsername) - assertThat(historicalIds).doesNotContain(context.trace.invocationId) - historicalIds.add(context.trace.invocationId) + // Create a Cash.State object for the StandaloneCashRpcClient to get + node.services.startFlow(CashIssueFlow(100.POUNDS, OpaqueBytes.of(1), identity), InvocationContext.shell()).flatMap { it.resultFuture }.getOrThrow() + val outOfProcessRpc = ProcessUtilities.startJavaProcess( + classpath = classpathWithoutFinance, + arguments = listOf(node.internals.configuration.rpcOptions.address.toString(), financeLocation) + ) + assertThat(outOfProcessRpc.waitFor()).isZero() // i.e. no exceptions were thrown + } + + private fun checkShellNotification(info: StateMachineInfo) { + val context = info.invocationContext + assertThat(context.origin).isInstanceOf(InvocationOrigin.Shell::class.java) + } + + private fun checkRpcNotification(info: StateMachineInfo, + rpcUsername: String, + historicalIds: MutableSet, + externalTrace: Trace?, + impersonatedActor: Actor?) { + val context = info.invocationContext + assertThat(context.origin).isInstanceOf(InvocationOrigin.RPC::class.java) + assertThat(context.externalTrace).isEqualTo(externalTrace) + assertThat(context.impersonatedActor).isEqualTo(impersonatedActor) + assertThat(context.actor?.id?.value).isEqualTo(rpcUsername) + assertThat(historicalIds).doesNotContain(context.trace.invocationId) + historicalIds.add(context.trace.invocationId) + } + + private object StandaloneCashRpcClient { + @JvmStatic + fun main(args: Array) { + checkNotOnClasspath("net.corda.finance.contracts.asset.Cash") { + "The finance module cannot be on the system classpath" + } + val address = NetworkHostAndPort.parse(args[0]) + val financeClassLoader = URLClassLoader(arrayOf(Paths.get(args[1]).toUri().toURL())) + val rpcUser = CordaRPCClientTest.rpcUser + val client = createCordaRPCClientWithSslAndClassLoader(address, classLoader = financeClassLoader) + val state = client.use(rpcUser.username, rpcUser.password) { + // financeClassLoader should be allowing the Cash.State to materialise + @Suppress("DEPRECATION") + it.proxy.internalVerifiedTransactionsSnapshot()[0].tx.outputsOfType>()[0] + } + assertThat(state.javaClass.name).isEqualTo("net.corda.finance.contracts.asset.Cash${'$'}State") + assertThat(state.amount.quantity).isEqualTo(10000) + assertThat(state.amount.token.product).isEqualTo(Currency.getInstance("GBP")) + // This particular check assures us that the Cash.State we have hasn't been carpented. + assertThat(state.participants).isEqualTo(listOf(state.owner)) + } + } } diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt index 76ff61a512..db41145f71 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt @@ -305,7 +305,7 @@ class CordaRPCClient private constructor( effectiveSerializationEnv } catch (e: IllegalStateException) { try { - AMQPClientSerializationScheme.initialiseSerialization() + AMQPClientSerializationScheme.initialiseSerialization(classLoader) } catch (e: IllegalStateException) { // Race e.g. two of these constructed in parallel, ignore. } diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt index 00613e9549..0728820cd5 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt @@ -211,7 +211,7 @@ class RPCClientProxyHandler( ThreadFactoryBuilder().setNameFormat("rpc-client-reaper-%d").setDaemon(true).build() ) sendExecutor = Executors.newSingleThreadExecutor( - ThreadFactoryBuilder().setNameFormat("rpc-client-sender-%d").build() + ThreadFactoryBuilder().setNameFormat("rpc-client-sender-%d").setDaemon(true).build() ) reaperScheduledFuture = reaperExecutor!!.scheduleAtFixedRate( this::reapObservablesAndNotify, diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt index 389609f84c..1f82119356 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt @@ -3,6 +3,7 @@ package net.corda.client.rpc.internal.serialization.amqp import net.corda.core.cordapp.Cordapp import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.SerializationContext.* import net.corda.core.serialization.SerializationCustomSerializer import net.corda.core.serialization.internal.SerializationEnvironment import net.corda.core.serialization.internal.SerializationEnvironmentImpl @@ -29,25 +30,26 @@ class AMQPClientSerializationScheme( companion object { /** Call from main only. */ - fun initialiseSerialization() { - nodeSerializationEnv = createSerializationEnv() + fun initialiseSerialization(classLoader: ClassLoader? = null) { + nodeSerializationEnv = createSerializationEnv(classLoader) } - fun createSerializationEnv(): SerializationEnvironment { + fun createSerializationEnv(classLoader: ClassLoader? = null): SerializationEnvironment { return SerializationEnvironmentImpl( SerializationFactoryImpl().apply { registerScheme(AMQPClientSerializationScheme(emptyList())) }, storageContext = AMQP_STORAGE_CONTEXT, - p2pContext = AMQP_P2P_CONTEXT, + p2pContext = if (classLoader != null) AMQP_P2P_CONTEXT.withClassLoader(classLoader) else AMQP_P2P_CONTEXT, rpcClientContext = AMQP_RPC_CLIENT_CONTEXT, - rpcServerContext = AMQP_RPC_SERVER_CONTEXT) + rpcServerContext = AMQP_RPC_SERVER_CONTEXT + ) } } - override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase) = - magic == amqpMagic && ( - target == SerializationContext.UseCase.RPCClient || target == SerializationContext.UseCase.P2P) + override fun canDeserializeVersion(magic: CordaSerializationMagic, target: SerializationContext.UseCase): Boolean { + return magic == amqpMagic && (target == UseCase.RPCClient || target == UseCase.P2P) + } override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory { return SerializerFactory(context.whitelist, ClassLoader.getSystemClassLoader(), context.lenientCarpenterEnabled).apply { @@ -60,4 +62,4 @@ class AMQPClientSerializationScheme( override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory { throw UnsupportedOperationException() } -} \ No newline at end of file +} diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt index 379257b20e..8f4439892d 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt @@ -222,7 +222,7 @@ class StandaloneCordaRPClientTest { assertEquals(1, queryResults.totalStatesAvailable) assertEquals(queryResults.states.first().state.data.amount.quantity, 629.POUNDS.quantity) - rpcProxy.startFlow(::CashPaymentFlow, 100.POUNDS, notaryNodeIdentity).returnValue.getOrThrow() + rpcProxy.startFlow(::CashPaymentFlow, 100.POUNDS, notaryNodeIdentity, true, notaryNodeIdentity).returnValue.getOrThrow() val moreResults = rpcProxy.vaultQueryBy(criteria, paging, sorting) assertEquals(3, moreResults.totalStatesAvailable) // 629 - 100 + 100 diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt index 0a81d244e1..760e2f2ee9 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt @@ -34,9 +34,8 @@ data class TransactionState @JvmOverloads constructor( * Currently these are loaded from the classpath of the node which includes the cordapp directory - at some * point these will also be loaded and run from the attachment store directly, allowing contracts to be * sent across, and run, from the network from within a sandbox environment. - * - * TODO: Implement the contract sandbox loading of the contract attachments - * */ + */ + // TODO: Implement the contract sandbox loading of the contract attachments val contract: ContractClassName, /** Identity of the notary that ensures the state is not used as an input to a transaction more than once */ val notary: Party, diff --git a/docs/source/getting-set-up.rst b/docs/source/getting-set-up.rst index 8e0c5f09e2..7a681a1b40 100644 --- a/docs/source/getting-set-up.rst +++ b/docs/source/getting-set-up.rst @@ -79,7 +79,7 @@ IntelliJ Download a sample project ^^^^^^^^^^^^^^^^^^^^^^^^^ 1. Open a command prompt -2. Clone the CorDapp example repo by running ``git clone https://github.com/corda/cordapp-example`` +2. Clone the ``cordapp-example`` repo by running ``git clone https://github.com/corda/cordapp-example`` 3. Move into the ``cordapp-example`` folder by running ``cd cordapp-example`` 4. Checkout the branch for Corda Enterprise 3.0.0 by running ``git checkout release-enterprise-V3`` @@ -147,7 +147,7 @@ IntelliJ Download a sample project ^^^^^^^^^^^^^^^^^^^^^^^^^ 1. Open a terminal -2. Clone the CorDapp example repo by running ``git clone https://github.com/corda/cordapp-example`` +2. Clone the ``cordapp-example`` repo by running ``git clone https://github.com/corda/cordapp-example`` 3. Move into the ``cordapp-example`` folder by running ``cd cordapp-example`` 4. Checkout the branch for Corda Enterprise 3.0.0 by running ``git checkout release-enterprise-V3`` @@ -193,31 +193,11 @@ Run from IntelliJ 7. Wait until the run windows displays the message ``Webserver started up in XX.X sec`` 8. Confirm that the CorDapp is running correctly by visiting the front end at http://localhost:10009/web/example/ -Corda source code ------------------ - -The Corda platform source code is available here: - - https://github.com/corda/corda.git - -A CorDapp template that you can use as the basis for your own CorDapps is available in both Java and Kotlin versions: - - https://github.com/corda/cordapp-template-java.git - - https://github.com/corda/cordapp-template-kotlin.git - -And a list of simple sample CorDapps for you to explore basic concepts is available here: - - https://www.corda.net/samples/ - -You can clone these repos to your local machine by running the command ``git clone [repo URL]``. - Next steps ---------- -The best way to check that everything is working fine is by taking a deeper look at the -:doc:`example CorDapp `. +First, explore the example CorDapp you just ran :doc:`here `. -Next, you should read through :doc:`Corda Key Concepts ` to understand how Corda works. +Next, read through :doc:`Corda Key Concepts ` to understand how Corda works. By then, you'll be ready to start writing your own CorDapps. Learn how to do this in the :doc:`Hello, World tutorial `. You may want to refer to the API documentation, the diff --git a/docs/source/node-database.rst b/docs/source/node-database.rst index 4cd656f9ac..7cbde0d287 100644 --- a/docs/source/node-database.rst +++ b/docs/source/node-database.rst @@ -290,7 +290,7 @@ To delete existing data from the database, run the following SQL: .. _postgres_ref: PostgreSQL -```````````````````````` +`````````` Corda has been tested on PostgreSQL 9.6 database, using PostgreSQL JDBC Driver 42.1.4. To set up a database schema, use the following SQL: diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index af281a5bd8..6479f6a3cd 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -12,6 +12,7 @@ package net.corda.node.internal import com.jcabi.manifests.Manifests import com.typesafe.config.Config +import com.typesafe.config.ConfigException import com.typesafe.config.ConfigRenderOptions import io.netty.channel.unix.Errors import net.corda.core.cordapp.Cordapp @@ -119,8 +120,16 @@ open class NodeStartup(val args: Array) { } catch (e: UnknownConfigurationKeysException) { logger.error(e.message) return false + } catch (e: ConfigException.IO) { + println(""" + Unable to load the node config file from '${cmdlineOptions.configFile}'. + + Try experimenting with the --base-directory flag to change which directory the node + is looking in, or use the --config-file flag to specify it explicitly. + """.trimIndent()) + return false } catch (e: Exception) { - logger.error("Exception during node configuration", e) + logger.error("Unexpected error whilst reading node configuration", e) return false } val errors = conf.validate() diff --git a/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt b/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt index f262a7103b..58912b3638 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt @@ -133,8 +133,12 @@ abstract class AppendOnlyPersistentMapBase( } protected fun loadValue(key: K): V? { - val result = currentDBSession().find(persistentEntityClass, toPersistentEntityKey(key)) - return result?.apply { currentDBSession().detach(result) }?.let(fromPersistentEntity)?.second + val session = currentDBSession() + // IMPORTANT: The flush is needed because detach() makes the queue of unflushed entries invalid w.r.t. Hibernate internal state if the found entity is unflushed. + // We want the detach() so that we rely on our cache memory management and don't retain strong references in the Hibernate session. + session.flush() + val result = session.find(persistentEntityClass, toPersistentEntityKey(key)) + return result?.apply { session.detach(result) }?.let(fromPersistentEntity)?.second } operator fun contains(key: K) = get(key) != null diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt index f4043673c9..7d7198950f 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt @@ -84,11 +84,11 @@ open class SerializerFactory( @DeleteForDJVM constructor(whitelist: ClassWhitelist, - classLoader: ClassLoader, + carpenterClassLoader: ClassLoader, lenientCarpenter: Boolean = false, evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(), fingerPrinter: FingerPrinter = SerializerFingerPrinter() - ) : this(whitelist, ClassCarpenterImpl(whitelist, classLoader, lenientCarpenter), evolutionSerializerGetter, fingerPrinter) + ) : this(whitelist, ClassCarpenterImpl(whitelist, carpenterClassLoader, lenientCarpenter), evolutionSerializerGetter, fingerPrinter) init { fingerPrinter.setOwner(this) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ProcessUtilities.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ProcessUtilities.kt index fa97cd88cc..ae2c0502ca 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ProcessUtilities.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/ProcessUtilities.kt @@ -11,17 +11,16 @@ package net.corda.testing.node.internal import net.corda.core.internal.div -import net.corda.core.internal.exists -import java.io.File.pathSeparator import java.nio.file.Path object ProcessUtilities { inline fun startJavaProcess( arguments: List, + classpath: String = defaultClassPath, jdwpPort: Int? = null, extraJvmArguments: List = emptyList() ): Process { - return startJavaProcessImpl(C::class.java.name, arguments, defaultClassPath, jdwpPort, extraJvmArguments, null, null) + return startJavaProcessImpl(C::class.java.name, arguments, classpath, jdwpPort, extraJvmArguments, null, null) } fun startCordaProcess( diff --git a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt index 6d92fe9bfd..dd16e9db7f 100644 --- a/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt +++ b/testing/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt @@ -18,6 +18,7 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.contextLogger import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.testing.common.internal.asContextEnv +import net.corda.testing.common.internal.checkNotOnClasspath import net.corda.testing.common.internal.testNetworkParameters import java.io.File import java.nio.file.Path @@ -75,18 +76,15 @@ class NodeProcess( val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(systemDefault()) val defaultNetworkParameters = run { AMQPClientSerializationScheme.createSerializationEnv().asContextEnv { - // There are no notaries in the network parameters for smoke test nodes. If this is required then we would + // TODO There are no notaries in the network parameters for smoke test nodes. If this is required then we would // need to introduce the concept of a "network" which predefines the notaries, like the driver and MockNetwork NetworkParametersCopier(testNetworkParameters()) } } init { - try { - Class.forName("net.corda.node.Corda") - throw Error("Smoke test has the node in its classpath. Please remove the offending dependency.") - } catch (e: ClassNotFoundException) { - // If the class can't be found then we're good! + checkNotOnClasspath("net.corda.node.Corda") { + "Smoke test has the node in its classpath. Please remove the offending dependency." } } } diff --git a/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/TestCommonUtils.kt b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/TestCommonUtils.kt new file mode 100644 index 0000000000..5ae693498e --- /dev/null +++ b/testing/test-common/src/main/kotlin/net/corda/testing/common/internal/TestCommonUtils.kt @@ -0,0 +1,10 @@ +package net.corda.testing.common.internal + +inline fun checkNotOnClasspath(className: String, errorMessage: () -> Any) { + try { + Class.forName(className) + throw IllegalStateException(errorMessage().toString()) + } catch (e: ClassNotFoundException) { + // If the class can't be found then we're good! + } +} diff --git a/tools/demobench/build.gradle b/tools/demobench/build.gradle index 0500652ea4..aa0920d2ce 100644 --- a/tools/demobench/build.gradle +++ b/tools/demobench/build.gradle @@ -46,8 +46,8 @@ repositories { flatDir { dirs 'libs' } + jcenter() maven { - jcenter() url 'http://www.sparetimelabs.com/maven2' } } diff --git a/tools/demobench/package-demobench-exe.bat b/tools/demobench/package-demobench-exe.bat index c2f0b41e80..5cbfd851a6 100644 --- a/tools/demobench/package-demobench-exe.bat +++ b/tools/demobench/package-demobench-exe.bat @@ -11,7 +11,7 @@ if "%DIRNAME%" == "" set DIRNAME=. call %DIRNAME%\..\..\gradlew -PpackageType=exe javapackage %* if ERRORLEVEL 1 goto Fail @echo -@echo Wrote installer to %DIRNAME%\build\javapackage\bundles\ +@echo Wrote installer to %DIRNAME%build\javapackage\bundles\ @echo goto end diff --git a/tools/demobench/package/bugfixes/apply.bat b/tools/demobench/package/bugfixes/apply.bat index 747dc67852..ba500032c1 100644 --- a/tools/demobench/package/bugfixes/apply.bat +++ b/tools/demobench/package/bugfixes/apply.bat @@ -29,14 +29,14 @@ if exist "%BUILDDIR%" rmdir /s /q "%BUILDDIR%" mkdir "%BUILDDIR%" for /r "%SOURCEDIR%" %%j in (*.java) do ( - javac -O -d "%BUILDDIR%" "%%j" + "%JAVA_HOME%\bin\javac" -O -d "%BUILDDIR%" "%%j" if ERRORLEVEL 1 ( @echo "Failed to compile %%j" exit /b 1 ) ) -jar uvf %1 -C "%BUILDDIR%" . +"%JAVA_HOME%\bin\jar" uvf %1 -C "%BUILDDIR%" . if ERRORLEVEL 1 ( @echo "Failed to update %1" exit /b 1 diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt index f66fbde2a4..0513b3a071 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt @@ -10,10 +10,8 @@ package net.corda.demobench.model -import com.typesafe.config.Config +import com.typesafe.config.* import com.typesafe.config.ConfigFactory.empty -import com.typesafe.config.ConfigRenderOptions -import com.typesafe.config.ConfigValueFactory import net.corda.core.identity.CordaX500Name import net.corda.core.internal.copyToDirectory import net.corda.core.internal.createDirectories @@ -55,13 +53,22 @@ data class NodeConfig( } fun nodeConf(): Config { - - val basic = NodeConfigurationData(myLegalName, p2pAddress, rpcAddress, notary, h2port, rpcUsers, useTestClock, detectPublicIp, devMode).toConfig() - val rpcSettings = empty() - .withValue("address", ConfigValueFactory.fromAnyRef(rpcAddress.toString())) - .withValue("adminAddress", ConfigValueFactory.fromAnyRef(rpcAdminAddress.toString())) - .root() - return basic.withoutPath("rpcAddress").withoutPath("rpcAdminAddress").withValue("rpcSettings", rpcSettings) + val rpcSettings: ConfigObject = empty() + .withValue("address", valueFor(rpcAddress.toString())) + .withValue("adminAddress", valueFor(rpcAdminAddress.toString())) + .root() + val customMap: Map = HashMap().also { + if (issuableCurrencies.isNotEmpty()) { + it["issuableCurrencies"] = issuableCurrencies + } + } + val custom: ConfigObject = ConfigFactory.parseMap(customMap).root() + return NodeConfigurationData(myLegalName, p2pAddress, rpcAddress, notary, h2port, rpcUsers, useTestClock, detectPublicIp, devMode) + .toConfig() + .withoutPath("rpcAddress") + .withoutPath("rpcAdminAddress") + .withValue("rpcSettings", rpcSettings) + .withOptionalValue("custom", custom) } fun webServerConf() = WebServerConfigurationData(myLegalName, rpcAddress, webAddress, rpcUsers).asConfig() @@ -70,33 +77,29 @@ data class NodeConfig( fun toWebServerConfText() = webServerConf().render() - fun serialiseAsString(): String { - - return toConfig().render() - } + fun serialiseAsString(): String = toConfig().render() private fun Config.render(): String = root().render(renderOptions) } private data class NodeConfigurationData( - val myLegalName: CordaX500Name, - val p2pAddress: NetworkHostAndPort, - val rpcAddress: NetworkHostAndPort, - val notary: NotaryService?, - val h2port: Int, - val rpcUsers: List = listOf(NodeConfig.defaultUser), - val useTestClock: Boolean, - val detectPublicIp: Boolean, - val devMode: Boolean + val myLegalName: CordaX500Name, + val p2pAddress: NetworkHostAndPort, + val rpcAddress: NetworkHostAndPort, + val notary: NotaryService?, + val h2port: Int, + val rpcUsers: List = listOf(NodeConfig.defaultUser), + val useTestClock: Boolean, + val detectPublicIp: Boolean, + val devMode: Boolean ) private data class WebServerConfigurationData( - val myLegalName: CordaX500Name, - val rpcAddress: NetworkHostAndPort, - val webAddress: NetworkHostAndPort, - val rpcUsers: List + val myLegalName: CordaX500Name, + val rpcAddress: NetworkHostAndPort, + val webAddress: NetworkHostAndPort, + val rpcUsers: List ) { - fun asConfig() = toConfig() } @@ -127,3 +130,9 @@ data class NodeConfigWrapper(val baseDir: Path, val nodeConfig: NodeConfig) : Ha fun user(name: String) = User(name, "letmein", setOf("ALL")) fun String.toKey() = filter { !it.isWhitespace() }.toLowerCase() + +fun valueFor(any: T): ConfigValue = ConfigValueFactory.fromAnyRef(any) + +private fun Config.withOptionalValue(path: String, obj: ConfigObject): Config { + return if (obj.isEmpty()) this else this.withValue(path, obj) +} diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt index 75d5a37fa1..ee066821cc 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt @@ -10,21 +10,18 @@ package net.corda.demobench.model +import com.typesafe.config.ConfigException import com.typesafe.config.ConfigFactory -import com.typesafe.config.ConfigValueFactory import net.corda.core.identity.CordaX500Name import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.config.parseAsNodeConfiguration import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.persistence.CordaPersistence.DataSourceConfigTag import net.corda.webserver.WebServerConfig -import org.assertj.core.api.Assertions.assertThat import org.junit.Test import java.nio.file.Path import java.nio.file.Paths -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue +import kotlin.test.* class NodeConfigTest { companion object { @@ -35,23 +32,26 @@ class NodeConfigTest { @Test fun `reading node configuration`() { val config = createConfig( - legalName = myLegalName, - p2pPort = 10001, - rpcPort = 40002, - rpcAdminPort = 40005, - webPort = 20001, - h2port = 30001, - notary = NotaryService(validating = false), - users = listOf(user("jenny")) + legalName = myLegalName, + p2pPort = 10001, + rpcPort = 40002, + rpcAdminPort = 40005, + webPort = 20001, + h2port = 30001, + notary = NotaryService(validating = false), + users = listOf(user("jenny")) ) val nodeConfig = config.nodeConf() - .withValue("baseDirectory", ConfigValueFactory.fromAnyRef(baseDir.toString())) - .withFallback(ConfigFactory.parseResources("reference.conf")) - .withFallback(ConfigFactory.parseMap(mapOf("devMode" to true))) - .resolve() + .withValue("baseDirectory", valueFor(baseDir.toString())) + .withFallback(ConfigFactory.parseResources("reference.conf")) + .withFallback(ConfigFactory.parseMap(mapOf("devMode" to true))) + .resolve() val fullConfig = nodeConfig.parseAsNodeConfiguration() + // No custom configuration is created by default. + assertFailsWith { nodeConfig.getConfig("custom") } + assertEquals(myLegalName, fullConfig.myLegalName) assertEquals(localPort(40002), fullConfig.rpcOptions.address) assertEquals(localPort(10001), fullConfig.p2pAddress) @@ -60,25 +60,49 @@ class NodeConfigTest { assertFalse(fullConfig.detectPublicIp) } + @Test + fun `reading node configuration with currencies`() { + val config = createConfig( + legalName = myLegalName, + p2pPort = 10001, + rpcPort = 10002, + rpcAdminPort = 10003, + webPort = 10004, + h2port = 10005, + notary = NotaryService(validating = false), + issuableCurrencies = listOf("GBP") + ) + + val nodeConfig = config.nodeConf() + .withValue("baseDirectory", valueFor(baseDir.toString())) + .withFallback(ConfigFactory.parseResources("reference.conf")) + .resolve() + val custom = nodeConfig.getConfig("custom") + assertEquals(listOf("GBP"), custom.getAnyRefList("issuableCurrencies")) + } + @Test fun `reading webserver configuration`() { val config = createConfig( - legalName = myLegalName, - p2pPort = 10001, - rpcPort = 40002, - rpcAdminPort = 40003, - webPort = 20001, - h2port = 30001, - notary = NotaryService(validating = false), - users = listOf(user("jenny")) + legalName = myLegalName, + p2pPort = 10001, + rpcPort = 40002, + rpcAdminPort = 40003, + webPort = 20001, + h2port = 30001, + notary = NotaryService(validating = false), + users = listOf(user("jenny")) ) val nodeConfig = config.webServerConf() - .withValue("baseDirectory", ConfigValueFactory.fromAnyRef(baseDir.toString())) - .withFallback(ConfigFactory.parseResources("web-reference.conf")) - .resolve() + .withValue("baseDirectory", valueFor(baseDir.toString())) + .withFallback(ConfigFactory.parseResources("web-reference.conf")) + .resolve() val webConfig = WebServerConfig(baseDir, nodeConfig) + // No custom configuration is created by default. + assertFailsWith { nodeConfig.getConfig("custom") } + assertEquals(localPort(20001), webConfig.webAddress) assertEquals(localPort(40002), webConfig.rpcAddress) assertEquals("trustpass", webConfig.trustStorePassword) @@ -86,24 +110,26 @@ class NodeConfigTest { } private fun createConfig( - legalName: CordaX500Name = CordaX500Name(organisation = "Unknown", locality = "Nowhere", country = "GB"), - p2pPort: Int = -1, - rpcPort: Int = -1, - rpcAdminPort: Int = -1, - webPort: Int = -1, - h2port: Int = -1, - notary: NotaryService?, - users: List = listOf(user("guest")) + legalName: CordaX500Name = CordaX500Name(organisation = "Unknown", locality = "Nowhere", country = "GB"), + p2pPort: Int = -1, + rpcPort: Int = -1, + rpcAdminPort: Int = -1, + webPort: Int = -1, + h2port: Int = -1, + notary: NotaryService?, + users: List = listOf(user("guest")), + issuableCurrencies: List = emptyList() ): NodeConfig { return NodeConfig( - myLegalName = legalName, - p2pAddress = localPort(p2pPort), - rpcAddress = localPort(rpcPort), - rpcAdminAddress = localPort(rpcAdminPort), - webAddress = localPort(webPort), - h2port = h2port, - notary = notary, - rpcUsers = users + myLegalName = legalName, + p2pAddress = localPort(p2pPort), + rpcAddress = localPort(rpcPort), + rpcAdminAddress = localPort(rpcAdminPort), + webAddress = localPort(webPort), + h2port = h2port, + notary = notary, + rpcUsers = users, + issuableCurrencies = issuableCurrencies ) } diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt index 09916ed81e..5d26cd4b1e 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt @@ -25,18 +25,10 @@ import net.corda.core.CordaException import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.UniqueIdentifier import net.corda.core.flows.FlowLogic -import net.corda.core.internal.Emoji +import net.corda.core.internal.* import net.corda.core.internal.concurrent.doneFuture import net.corda.core.internal.concurrent.openFuture -import net.corda.core.internal.createDirectories -import net.corda.core.internal.div -import net.corda.core.internal.rootCause -import net.corda.core.internal.uncheckedCast -import net.corda.core.messaging.CordaRPCOps -import net.corda.core.messaging.DataFeed -import net.corda.core.messaging.FlowProgressHandle -import net.corda.core.messaging.StateMachineUpdate -import net.corda.core.messaging.pendingFlowsCount +import net.corda.core.messaging.* import net.corda.tools.shell.utlities.ANSIProgressRenderer import net.corda.tools.shell.utlities.StdoutANSIProgressRenderer import org.crsh.command.InvocationContext @@ -141,8 +133,7 @@ object InteractiveShell { config["crash.ssh.port"] = configuration.sshdPort?.toString() config["crash.auth"] = "corda" configuration.sshHostKeyDirectory?.apply { - val sshKeysDir = configuration.sshHostKeyDirectory - sshKeysDir.createDirectories() + val sshKeysDir = configuration.sshHostKeyDirectory.createDirectories() config["crash.ssh.keypath"] = (sshKeysDir / "hostkey.pem").toString() config["crash.ssh.keygen"] = "true" } @@ -285,7 +276,7 @@ object InteractiveShell { val stateObservable = runFlowFromString({ clazz, args -> rpcOps.startTrackedFlowDynamic(clazz, *args) }, inputData, flowClazz, om) val latch = CountDownLatch(1) - ansiProgressRenderer.render(stateObservable, { latch.countDown() }) + ansiProgressRenderer.render(stateObservable, latch::countDown) // Wait for the flow to end and the progress tracker to notice. By the time the latch is released // the tracker is done with the screen. while (!Thread.currentThread().isInterrupted) { @@ -301,11 +292,7 @@ object InteractiveShell { } } } - stateObservable.returnValue.get()?.apply { - if (this !is Throwable) { - output.println("Flow completed with result: $this") - } - } + output.println("Flow completed with result: ${stateObservable.returnValue.get()}") } catch (e: NoApplicableConstructor) { output.println("No matching constructor found:", Color.red) e.errors.forEach { output.println("- $it", Color.red) }