mirror of
https://github.com/corda/corda.git
synced 2025-03-14 16:26:36 +00:00
Merge remote-tracking branch 'open/master' into anthony-os-merge-2018-10-24
# Conflicts: # build.gradle # core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt # core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt # docs/source/cli-ux-guidelines.rst # docs/source/testnet-explorer-corda.rst # finance/src/integration-test/kotlin/net/corda/finance/flows/test/CashConfigDataFlowTest.kt # finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt # node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowVersioningTest.kt # node/src/main/kotlin/net/corda/node/Corda.kt # node/src/main/kotlin/net/corda/node/internal/Node.kt # node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt # node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt # node/src/test/kotlin/net/corda/node/internal/NodeTest.kt # testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt # testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt # tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt
This commit is contained in:
commit
08f32a9329
@ -180,7 +180,7 @@ see changes to this list.
|
||||
* Scott James
|
||||
* Sean Zhang (Wells Fargo)
|
||||
* Shams Asari (R3)
|
||||
* Shivan Sawant (Persistent Systems Limited)
|
||||
* Shivan Sawant
|
||||
* Siddhartha Sengupta (Tradewind Markets)
|
||||
* Simon Taylor (Barclays)
|
||||
* Sofus Mortensen (Digital Asset Holdings)
|
||||
|
@ -77,7 +77,7 @@ buildscript {
|
||||
ext.snappy_version = '0.4'
|
||||
ext.class_graph_version = '4.2.12'
|
||||
ext.jcabi_manifests_version = '1.1'
|
||||
ext.picocli_version = '3.5.2'
|
||||
ext.picocli_version = '3.6.1'
|
||||
|
||||
// Name of the IntelliJ SDK created for the deterministic Java rt.jar.
|
||||
// ext.deterministic_idea_sdk = '1.8 (Deterministic)'
|
||||
|
@ -1,4 +1,4 @@
|
||||
gradlePluginsVersion=4.0.32
|
||||
gradlePluginsVersion=4.0.33
|
||||
kotlinVersion=1.2.71
|
||||
# ***************************************************************#
|
||||
# When incrementing platformVersion make sure to update #
|
||||
|
@ -1,7 +1,6 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.KeepForDJVM
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.extractFile
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.io.FileNotFoundException
|
||||
|
@ -28,6 +28,14 @@ object JarSignatureCollector {
|
||||
|
||||
fun collectSigningParties(jar: JarInputStream): List<Party> = getSigners(jar).toPartiesOrderedByName()
|
||||
|
||||
/**
|
||||
* Returns an ordered list of every [X509Certificate] which has signed every signable item in the given [JarInputStream].
|
||||
*
|
||||
* @param jar The open [JarInputStream] to collect signing parties from.
|
||||
* @throws InvalidJarSignersException If the signer sets for any two signable items are different from each other.
|
||||
*/
|
||||
fun collectCertificates(jar: JarInputStream): List<X509Certificate> = getSigners(jar).toCertificates()
|
||||
|
||||
private fun getSigners(jar: JarInputStream): Set<CodeSigner> {
|
||||
val signerSets = jar.fileSignerSets
|
||||
if (signerSets.isEmpty()) return emptySet()
|
||||
@ -71,6 +79,10 @@ object JarSignatureCollector {
|
||||
Party(it.signerCertPath.certificates[0] as X509Certificate)
|
||||
}.sortedBy { it.name.toString() } // Sorted for determinism.
|
||||
|
||||
private fun Set<CodeSigner>.toCertificates(): List<X509Certificate> = map {
|
||||
it.signerCertPath.certificates[0] as X509Certificate
|
||||
}.sortedBy { it.toString() } // Sorted for determinism.
|
||||
|
||||
private val JarInputStream.entries get(): Sequence<JarEntry> = generateSequence(nextJarEntry) { nextJarEntry }
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ data class CordappImpl(
|
||||
*/
|
||||
override val cordappClasses: List<String> = run {
|
||||
val classList = rpcFlows + initiatedFlows + services + serializationWhitelists.map { javaClass } + notaryService
|
||||
classList.mapNotNull { it?.name } + contractClassNames
|
||||
classList.mapNotNull { it?.name } + contractClassNames
|
||||
}
|
||||
|
||||
// TODO Why a seperate Info class and not just have the fields directly in CordappImpl?
|
||||
|
@ -4,7 +4,11 @@ import co.paralleluniverse.strands.Strand
|
||||
import net.corda.core.CordaInternal
|
||||
import net.corda.core.DeleteForDJVM
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.SignableData
|
||||
import net.corda.core.crypto.SignatureMetadata
|
||||
import net.corda.core.crypto.keys
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.AttachmentWithContext
|
||||
import net.corda.core.internal.FlowStateMachine
|
||||
@ -24,6 +28,38 @@ import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.Collection
|
||||
import kotlin.collections.List
|
||||
import kotlin.collections.Map
|
||||
import kotlin.collections.MutableList
|
||||
import kotlin.collections.Set
|
||||
import kotlin.collections.all
|
||||
import kotlin.collections.any
|
||||
import kotlin.collections.arrayListOf
|
||||
import kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
import kotlin.collections.emptyList
|
||||
import kotlin.collections.filter
|
||||
import kotlin.collections.filterNot
|
||||
import kotlin.collections.find
|
||||
import kotlin.collections.flatMap
|
||||
import kotlin.collections.flatten
|
||||
import kotlin.collections.forEach
|
||||
import kotlin.collections.groupBy
|
||||
import kotlin.collections.intersect
|
||||
import kotlin.collections.isNotEmpty
|
||||
import kotlin.collections.listOf
|
||||
import kotlin.collections.map
|
||||
import kotlin.collections.mapNotNull
|
||||
import kotlin.collections.none
|
||||
import kotlin.collections.plus
|
||||
import kotlin.collections.setOf
|
||||
import kotlin.collections.single
|
||||
import kotlin.collections.singleOrNull
|
||||
import kotlin.collections.toList
|
||||
import kotlin.collections.toMap
|
||||
import kotlin.collections.toSet
|
||||
import kotlin.collections.toSortedSet
|
||||
|
||||
/**
|
||||
* A TransactionBuilder is a transaction class that's mutable (unlike the others which are all immutable). It is
|
||||
|
@ -1,10 +1,13 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.internal.InitiatedFlowFactory
|
||||
import net.corda.testing.node.internal.TestStartedNode
|
||||
import rx.Observable
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
@ -34,20 +37,6 @@ class NoAnswer(private val closure: () -> Unit = {}) : FlowLogic<Unit>() {
|
||||
override fun call() = closure()
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to register a flow of type [R] against an initiating flow of type [I].
|
||||
*/
|
||||
inline fun <I : FlowLogic<*>, reified R : FlowLogic<*>> TestStartedNode.registerInitiatedFlow(initiatingFlowType: KClass<I>, crossinline construct: (session: FlowSession) -> R) {
|
||||
registerFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> construct(session) }, R::class.javaObjectType, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to register a flow of type [Answer] against an initiating flow of type [I], returning a valure of type [R].
|
||||
*/
|
||||
inline fun <I : FlowLogic<*>, reified R : Any> TestStartedNode.registerAnswer(initiatingFlowType: KClass<I>, value: R) {
|
||||
registerFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> Answer(session, value) }, Answer::class.javaObjectType, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts data from a [Map[FlowSession, UntrustworthyData<Any>]] without performing checks and casting to [R].
|
||||
*/
|
||||
@ -112,4 +101,23 @@ inline fun <reified R : Any> FlowLogic<*>.receiveAll(session: FlowSession, varar
|
||||
|
||||
private fun Array<out Pair<FlowSession, Class<out Any>>>.enforceNoDuplicates() {
|
||||
require(this.size == this.toSet().size) { "A flow session can only appear once as argument." }
|
||||
}
|
||||
|
||||
inline fun <reified P : FlowLogic<*>> TestStartedNode.registerCordappFlowFactory(
|
||||
initiatingFlowClass: KClass<out FlowLogic<*>>,
|
||||
initiatedFlowVersion: Int = 1,
|
||||
noinline flowFactory: (FlowSession) -> P): CordaFuture<P> {
|
||||
|
||||
val observable = internals.registerInitiatedFlowFactory(
|
||||
initiatingFlowClass.java,
|
||||
P::class.java,
|
||||
InitiatedFlowFactory.CorDapp(initiatedFlowVersion, "", flowFactory),
|
||||
track = true)
|
||||
return observable.toFuture()
|
||||
}
|
||||
|
||||
fun <T : FlowLogic<*>> TestStartedNode.registerCoreFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>,
|
||||
initiatedFlowClass: Class<T>,
|
||||
flowFactory: (FlowSession) -> T , track: Boolean): Observable<T> {
|
||||
return this.internals.registerInitiatedFlowFactory(initiatingFlowClass, initiatedFlowClass, InitiatedFlowFactory.Core(flowFactory), track)
|
||||
}
|
@ -2,16 +2,18 @@ package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.natpryce.hamkrest.assertion.assert
|
||||
import net.corda.testing.internal.matchers.flow.willReturn
|
||||
import net.corda.core.flows.mixins.WithMockNet
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.internal.matchers.flow.willReturn
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.internal.TestStartedNode
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.AfterClass
|
||||
import org.junit.Test
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
|
||||
class ReceiveMultipleFlowTests : WithMockNet {
|
||||
@ -43,7 +45,7 @@ class ReceiveMultipleFlowTests : WithMockNet {
|
||||
}
|
||||
}
|
||||
|
||||
nodes[1].registerInitiatedFlow(initiatingFlow::class) { session ->
|
||||
nodes[1].registerCordappFlowFactory(initiatingFlow::class) { session ->
|
||||
object : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
@ -123,4 +125,15 @@ class ReceiveMultipleFlowTests : WithMockNet {
|
||||
return double * string.length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T> TestStartedNode.registerAnswer(kClass: KClass<out FlowLogic<Any>>, value1: T) {
|
||||
this.registerCordappFlowFactory(kClass) { session ->
|
||||
object : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
session.send(value1!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,6 @@ import net.corda.core.JarSignatureTestUtils.updateJar
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.CHARLIE_NAME
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.AfterClass
|
||||
|
@ -3,16 +3,12 @@ package net.corda.core.serialization
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.TestNoSecurityDataVendingFlow
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.FetchAttachmentsFlow
|
||||
import net.corda.core.internal.FetchDataFlow
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.internal.InitiatedFlowFactory
|
||||
import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.nodeapi.internal.persistence.currentDBSession
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
@ -151,11 +147,10 @@ class AttachmentSerializationTest {
|
||||
}
|
||||
|
||||
private fun launchFlow(clientLogic: ClientLogic, rounds: Int, sendData: Boolean = false) {
|
||||
server.registerFlowFactory(
|
||||
ClientLogic::class.java,
|
||||
InitiatedFlowFactory.Core { ServerLogic(it, sendData) },
|
||||
ServerLogic::class.java,
|
||||
track = false)
|
||||
server.registerCordappFlowFactory(
|
||||
ClientLogic::class,
|
||||
1
|
||||
) { ServerLogic(it, sendData) }
|
||||
client.services.startFlow(clientLogic)
|
||||
mockNet.runNetwork(rounds)
|
||||
}
|
||||
|
@ -105,9 +105,9 @@ The blob inspector can be started with the following command-line options:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
blob-inspector [-hvV] [--full-parties] [--install-shell-extensions] [--schema]
|
||||
[--format=type] [--input-format=type]
|
||||
[--logging-level=<loggingLevel>] [SOURCE]
|
||||
blob-inspector [-hvV] [--full-parties] [--schema] [--format=type]
|
||||
[--input-format=type] [--logging-level=<loggingLevel>] SOURCE
|
||||
[COMMAND]
|
||||
|
||||
* ``--format=type``: Output format. Possible values: [YAML, JSON]. Default: YAML.
|
||||
* ``--input-format=type``: Input format. If the file can't be decoded with the given value it's auto-detected, so you should
|
||||
@ -116,6 +116,10 @@ The blob inspector can be started with the following command-line options:
|
||||
* ``--schema``: Print the blob's schema first.
|
||||
* ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file.
|
||||
* ``--logging-level=<loggingLevel>``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO.
|
||||
* ``--install-shell-extensions``: Install ``blob-inspector`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
|
||||
* ``--help``, ``-h``: Show this help message and exit.
|
||||
* ``--version``, ``-V``: Print version information and exit.
|
||||
* ``--version``, ``-V``: Print version information and exit.
|
||||
|
||||
Sub-commands
|
||||
^^^^^^^^^^^^
|
||||
|
||||
``install-shell-extensions``: Install ``blob-inspector`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
|
@ -1548,9 +1548,9 @@ New features in this release:
|
||||
|
||||
* Testnet
|
||||
|
||||
* Permissioning infrastructure phase one is built out. The node now has a notion of developer mode vs normal
|
||||
mode. In developer mode it works like M3 and the SSL certificates used by nodes running on your local
|
||||
machine all self-sign using a developer key included in the source tree. When developer mode is not active,
|
||||
* Permissioning infrastructure phase one is built out. The node now has a notion of development mode vs normal
|
||||
mode. In development mode it works like M3 and the SSL certificates used by nodes running on your local
|
||||
machine all self-sign using a developer key included in the source tree. When development mode is not active,
|
||||
the node won't start until it has a signed certificate. Such a certificate can be obtained by simply running
|
||||
an included command line utility which generates a CSR and submits it to a permissioning service, then waits
|
||||
for the signed certificate to be returned. Note that currently there is no public Corda testnet, so we are
|
||||
|
@ -10,7 +10,7 @@ Users of ``bash`` or ``zsh`` can install an alias and auto-completion for Corda
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
java -jar <name-of-JAR>.jar --install-shell-extensions
|
||||
java -jar <name-of-JAR>.jar install-shell-extensions
|
||||
|
||||
Then, either restart your shell, or for ``bash`` users run:
|
||||
|
||||
@ -34,7 +34,7 @@ For example, for the Corda node, install the shell extensions using
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
java -jar corda-<version>.jar --install-shell-extensions
|
||||
java -jar corda-<version>.jar install-shell-extensions
|
||||
|
||||
And then run the node by running:
|
||||
|
||||
|
@ -144,7 +144,7 @@ absolute path to the node's base directory.
|
||||
.. note:: The RPC SSL certificate is used by RPC clients to authenticate the connection.
|
||||
The Node operator must provide RPC clients with a truststore containing the certificate they can trust.
|
||||
We advise Node operators to not use the P2P keystore for RPC.
|
||||
The node ships with a command line argument "--just-generate-rpc-ssl-settings", which generates a secure keystore
|
||||
The node can be run with the "generate-rpc-ssl-settings" command, which generates a secure keystore
|
||||
and truststore that can be used to secure the RPC connection. You can use this if you have no special requirements.
|
||||
|
||||
|
||||
|
@ -46,8 +46,8 @@ threats is the [STRIDE](https://en.wikipedia.org/wiki/STRIDE_(security)) framewo
|
||||
|
||||
- Spoofing
|
||||
- Tampering
|
||||
- Information Disclosure
|
||||
- Repudiation
|
||||
- Information Disclosure
|
||||
- Denial of Service
|
||||
- Elevation of Privilege
|
||||
|
||||
|
141
docs/source/flow-overriding.rst
Normal file
141
docs/source/flow-overriding.rst
Normal file
@ -0,0 +1,141 @@
|
||||
Configuring Responder Flows
|
||||
===========================
|
||||
|
||||
A flow can be a fairly complex thing that interacts with many backend systems, and so it is likely that different users
|
||||
of a specific CordApp will require differences in how flows interact with their specific infrastructure.
|
||||
|
||||
Corda supports this functionality by providing two mechanisms to modify the behaviour of apps in your node.
|
||||
|
||||
Subclassing a Flow
|
||||
------------------
|
||||
|
||||
If you have a workflow which is mostly common, but also requires slight alterations in specific situations, most developers would be familiar
|
||||
with refactoring into `Base` and `Sub` classes. A simple example is shown below.
|
||||
|
||||
java
|
||||
~~~~
|
||||
|
||||
.. code-block:: java
|
||||
|
||||
@InitiatingFlow
|
||||
public class Initiator extends FlowLogic<String> {
|
||||
private final Party otherSide;
|
||||
|
||||
public Initiator(Party otherSide) {
|
||||
this.otherSide = otherSide;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String call() throws FlowException {
|
||||
return initiateFlow(otherSide).receive(String.class).unwrap((it) -> it);
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(Initiator.class)
|
||||
public class BaseResponder extends FlowLogic<Void> {
|
||||
private FlowSession counterpartySession;
|
||||
|
||||
public BaseResponder(FlowSession counterpartySession) {
|
||||
super();
|
||||
this.counterpartySession = counterpartySession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void call() throws FlowException {
|
||||
counterpartySession.send(getMessage());
|
||||
return Void;
|
||||
}
|
||||
|
||||
|
||||
protected String getMessage() {
|
||||
return "This Is the Legacy Responder";
|
||||
}
|
||||
}
|
||||
|
||||
public class SubResponder extends BaseResponder {
|
||||
|
||||
public SubResponder(FlowSession counterpartySession) {
|
||||
super(counterpartySession);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getMessage() {
|
||||
return "This is the sub responder";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
kotlin
|
||||
~~~~~~
|
||||
|
||||
.. code-block:: kotlin
|
||||
|
||||
@InitiatedBy(Initiator::class)
|
||||
open class BaseResponder(internal val otherSideSession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
otherSideSession.send(getMessage())
|
||||
}
|
||||
protected open fun getMessage() = "This Is the Legacy Responder"
|
||||
}
|
||||
|
||||
@InitiatedBy(Initiator::class)
|
||||
class SubResponder(otherSideSession: FlowSession) : BaseResponder(otherSideSession) {
|
||||
override fun getMessage(): String {
|
||||
return "This is the sub responder"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Corda would detect that both ``BaseResponder`` and ``SubResponder`` are configured for responding to ``Initiator``.
|
||||
Corda will then calculate the hops to ``FlowLogic`` and select the implementation which is furthest distance, ie: the most subclassed implementation.
|
||||
In the above example, ``SubResponder`` would be selected as the default responder for ``Initiator``
|
||||
|
||||
.. note:: The flows do not need to be within the same CordApp, or package, therefore to customise a shared app you obtained from a third party, you'd write your own CorDapp that subclasses the first."
|
||||
|
||||
Overriding a flow via node configuration
|
||||
----------------------------------------
|
||||
|
||||
Whilst the subclassing approach is likely to be useful for most applications, there is another mechanism to override this behaviour.
|
||||
This would be useful if for example, a specific CordApp user requires such a different responder that subclassing an existing flow
|
||||
would not be a good solution. In this case, it's possible to specify a hardcoded flow via the node configuration.
|
||||
|
||||
The configuration section is named ``flowOverrides`` and it accepts an array of ``overrides``
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
flowOverrides {
|
||||
overrides=[
|
||||
{
|
||||
initiator="net.corda.Initiator"
|
||||
responder="net.corda.BaseResponder"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
The cordform plugin also provides a ``flowOverride`` method within the ``deployNodes`` block which can be used to override a flow. In the below example, we will override
|
||||
the ``SubResponder`` with ``BaseResponder``
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
.. code-block:: groovy
|
||||
|
||||
node {
|
||||
name "O=Bank,L=London,C=GB"
|
||||
p2pPort 10025
|
||||
rpcUsers = ext.rpcUsers
|
||||
rpcSettings {
|
||||
address "localhost:10026"
|
||||
adminAddress "localhost:10027"
|
||||
}
|
||||
extraConfig = ['h2Settings.address' : 'localhost:10035']
|
||||
flowOverride("net.corda.Initiator", "net.corda.BaseResponder")
|
||||
}
|
||||
|
||||
This will generate the corresponding ``flowOverrides`` section and place it in the configuration for that node.
|
@ -255,14 +255,19 @@ The network bootstrapper can be started with the following command-line options:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
bootstrapper [-hvV] [--install-shell-extensions] [--no-copy] [--dir=<dir>]
|
||||
[--logging-level=<loggingLevel>]
|
||||
bootstrapper [-hvV] [--no-copy] [--dir=<dir>] [--logging-level=<loggingLevel>]
|
||||
[--minimum-platform-version=<minimumPlatformVersion>] [COMMAND]
|
||||
|
||||
* ``--dir=<dir>``: Root directory containing the node configuration files and CorDapp JARs that will form the test network.
|
||||
It may also contain existing node directories. Defaults to the current directory.
|
||||
* ``--no-copy``: Don't copy the CorDapp JARs into the nodes' "cordapps" directories.
|
||||
* ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file.
|
||||
* ``--logging-level=<loggingLevel>``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO.
|
||||
* ``--install-shell-extensions``: Install ``bootstrapper`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
|
||||
* ``--help``, ``-h``: Show this help message and exit.
|
||||
* ``--version``, ``-V``: Print version information and exit.
|
||||
* ``--version``, ``-V``: Print version information and exit.
|
||||
* ``--minimum-platform-version``: The minimum platform version to use in the generated network-parameters.
|
||||
|
||||
Sub-commands
|
||||
^^^^^^^^^^^^
|
||||
|
||||
``install-shell-extensions``: Install ``bootstrapper`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
|
@ -63,7 +63,7 @@ be used to supplement or replace the HTTP network map. If the same node is adver
|
||||
latest one is taken.
|
||||
|
||||
On startup the node generates its own signed node info file, filename of the format ``nodeInfo-${hash}``. It can also be
|
||||
generated using the ``--just-generate-node-info`` command line flag without starting the node. To create a simple network
|
||||
generated using the ``generate-node-info`` sub-command without starting the node. To create a simple network
|
||||
without the HTTP network map service simply place this file in the ``additional-node-infos`` directory of every node that's
|
||||
part of this network. For example, a simple way to do this is to use rsync.
|
||||
|
||||
@ -210,7 +210,7 @@ you either need to run from the command line:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
java -jar corda.jar --clear-network-map-cache
|
||||
java -jar corda.jar clear-network-cache
|
||||
|
||||
or call RPC method `clearNetworkMapCache` (it can be invoked through the node's shell as `run clearNetworkMapCache`, for more information on
|
||||
how to log into node's shell see :doc:`shell`). As we are testing and hardening the implementation this step shouldn't be required.
|
||||
|
@ -48,25 +48,38 @@ Command-line options
|
||||
The node can optionally be started with the following command-line options:
|
||||
|
||||
* ``--base-directory``, ``-b``: The node working directory where all the files are kept (default: ``.``).
|
||||
* ``--clear-network-map-cache``, ``-c``: Clears local copy of network map, on node startup it will be restored from server or file system.
|
||||
* ``--config-file``, ``-f``: The path to the config file. Defaults to ``node.conf``.
|
||||
* ``--dev-mode``, ``-d``: Runs the node in developer mode. Unsafe in production. Defaults to true on MacOS and desktop versions of Windows. False otherwise.
|
||||
* ``--initial-registration``: Start initial node registration with the compatibility zone to obtain a certificate from the Doorman.
|
||||
* ``--just-generate-node-info``: Perform the node start-up task necessary to generate its nodeInfo, save it to disk, then
|
||||
quit.
|
||||
* ``--just-generate-rpc-ssl-settings``: Generate the ssl keystore and truststore for a secure RPC connection.
|
||||
* ``--network-root-truststore``, ``-t``: Network root trust store obtained from network operator.
|
||||
* ``--network-root-truststore-password``, ``-p``: Network root trust store password obtained from network operator.
|
||||
* ``--dev-mode``, ``-d``: Runs the node in development mode. Unsafe in production. Defaults to true on MacOS and desktop versions of Windows. False otherwise.
|
||||
* ``--no-local-shell``, ``-n``: Do not start the embedded shell locally.
|
||||
* ``--on-unknown-config-keys <[FAIL,WARN,INFO]>``: How to behave on unknown node configuration. Defaults to FAIL.
|
||||
* ``--sshd``: Enables SSH server for node administration.
|
||||
* ``--sshd-port``: Sets the port for the SSH server. If not supplied and SSH server is enabled, the port defaults to 2222.
|
||||
* ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file.
|
||||
* ``--logging-level=<loggingLevel>``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO.
|
||||
* ``--install-shell-extensions``: Install ``corda`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
|
||||
* ``--help``, ``-h``: Show this help message and exit.
|
||||
* ``--version``, ``-V``: Print version information and exit.
|
||||
|
||||
Sub-commands
|
||||
^^^^^^^^^^^^
|
||||
|
||||
``bootstrap-raft-cluster``: Bootstraps Raft cluster. The node forms a single node cluster (ignoring otherwise configured peer
|
||||
addresses), acting as a seed for other nodes to join the cluster.
|
||||
|
||||
``clear-network-cache``: Clears local copy of network map, on node startup it will be restored from server or file system.
|
||||
|
||||
``initial-registration``: Starts initial node registration with the compatibility zone to obtain a certificate from the Doorman.
|
||||
|
||||
Parameters:
|
||||
|
||||
* ``--network-root-truststore``, ``-t`` **required**: Network root trust store obtained from network operator.
|
||||
* ``--network-root-truststore-password``, ``-p``: Network root trust store password obtained from network operator.
|
||||
|
||||
``generate-node-info``: Performs the node start-up tasks necessary to generate the nodeInfo file, saves it to disk, then exits.
|
||||
|
||||
``generate-rpc-ssl-settings``: Generates the SSL keystore and truststore for a secure RPC connection.
|
||||
|
||||
``install-shell-extensions``: Install ``corda`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
|
||||
|
||||
.. _enabling-remote-debugging:
|
||||
|
||||
Enabling remote debugging
|
||||
|
@ -107,28 +107,15 @@ Starting the standalone shell
|
||||
|
||||
Run the following command from the terminal:
|
||||
|
||||
Linux and MacOS
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
.. code:: bash
|
||||
|
||||
java -jar corda-tools-shell-cli-VERSION_NUMBER.jar [--config-file PATH | --cordpass-directory PATH --commands-directory PATH --host HOST --port PORT
|
||||
--user USER --password PASSWORD --sshd-port PORT --sshd-hostkey-directory PATH --keystore-password PASSWORD
|
||||
--keystore-file FILE --truststore-password PASSWORD --truststore-file FILE | --help]
|
||||
|
||||
Windows
|
||||
^^^^^^^
|
||||
|
||||
.. code:: bash
|
||||
|
||||
corda-shell [-hvV] [--install-shell-extensions]
|
||||
[--logging-level=<loggingLevel>] [--password=<password>]
|
||||
corda-shell [-hvV] [--logging-level=<loggingLevel>] [--password=<password>]
|
||||
[--sshd-hostkey-directory=<sshdHostKeyDirectory>]
|
||||
[--sshd-port=<sshdPort>] [--truststore-file=<trustStoreFile>]
|
||||
[--truststore-password=<trustStorePassword>]
|
||||
[--truststore-type=<trustStoreType>] [--user=<user>] [-a=<host>]
|
||||
[-c=<cordappDirectory>] [-f=<configFile>] [-o=<commandsDirectory>]
|
||||
[-p=<port>]
|
||||
[-p=<port>] [COMMAND]
|
||||
|
||||
Where:
|
||||
|
||||
@ -146,10 +133,11 @@ Where:
|
||||
* ``--truststore-type=<trustStoreType>``: The type of the TrustStore (e.g. JKS).
|
||||
* ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file.
|
||||
* ``--logging-level=<loggingLevel>``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO.
|
||||
* ``--install-shell-extensions``: Install ``corda-shell`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
|
||||
* ``--help``, ``-h``: Show this help message and exit.
|
||||
* ``--version``, ``-V``: Print version information and exit.
|
||||
|
||||
Additionally, the ``install-shell-extensions`` subcommand can be used to install the ``corda-shell`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info.
|
||||
|
||||
The format of ``config-file``:
|
||||
|
||||
.. code:: bash
|
||||
|
@ -21,6 +21,7 @@ class Configuration(
|
||||
nodeInterface.dbPort,
|
||||
password = DEFAULT_PASSWORD
|
||||
),
|
||||
// TODO This is not being used when it could be. The call-site is using configElements instead.
|
||||
val notary: NotaryConfiguration = NotaryConfiguration(),
|
||||
val cordapps: CordappConfiguration = CordappConfiguration(),
|
||||
vararg configElements: ConfigurationTemplate
|
||||
@ -28,9 +29,7 @@ class Configuration(
|
||||
|
||||
private val developerMode = System.getProperty("USE_NETWORK_SERVICES") == null
|
||||
|
||||
val cordaX500Name: CordaX500Name by lazy({
|
||||
CordaX500Name(name, location, country)
|
||||
})
|
||||
val cordaX500Name: CordaX500Name = CordaX500Name(name, location, country)
|
||||
|
||||
private val basicConfig = """
|
||||
|myLegalName="C=$country,L=$location,O=$name"
|
||||
|
@ -1,5 +1,7 @@
|
||||
package net.corda.behave.node.configuration
|
||||
|
||||
// TODO This is a ConfigurationTemplate but is never used as one. Therefore the private "applications" list is never used
|
||||
// and thus includeFinance isn't necessary either. Something is amiss.
|
||||
class CordappConfiguration(var apps: List<String> = emptyList(), val includeFinance: Boolean = false) : ConfigurationTemplate() {
|
||||
|
||||
private val applications = apps + if (includeFinance) {
|
||||
|
@ -7,6 +7,10 @@ class CurrencyConfiguration(private val issuableCurrencies: List<String>) : Conf
|
||||
if (issuableCurrencies.isEmpty()) {
|
||||
""
|
||||
} else {
|
||||
// TODO This is no longer correct. issuableCurrencies is a config of the finance app and belongs
|
||||
// in a separate .conf file for the app (in the config sub-directory, with a filename matching the CorDapp
|
||||
// jar filename). It is no longer read in from the node conf file. There seem to be pieces missing in the
|
||||
// behave framework to allow one to do this.
|
||||
"""
|
||||
|custom : {
|
||||
| issuableCurrencies : [
|
||||
|
@ -7,9 +7,9 @@ import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.flows.CashConfigDataFlow
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.finance.flows.CashPaymentFlow
|
||||
import net.corda.finance.internal.CashConfigDataFlow
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
@ -1,38 +0,0 @@
|
||||
package net.corda.finance.flows.test
|
||||
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.EUR
|
||||
import net.corda.finance.USD
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.DUMMY_BANK_A_NAME
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.finance.flows.CashConfigDataFlow
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.internal.IntegrationTest
|
||||
import net.corda.testing.internal.IntegrationTestSchemas
|
||||
import net.corda.testing.internal.toDatabaseSchemaName
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.ClassRule
|
||||
import org.junit.Test
|
||||
|
||||
class CashConfigDataFlowTest : IntegrationTest() {
|
||||
companion object {
|
||||
@ClassRule
|
||||
@JvmField
|
||||
val databaseSchemas = IntegrationTestSchemas(ALICE_NAME.toDatabaseSchemaName(), BOB_NAME.toDatabaseSchemaName(),
|
||||
DUMMY_BANK_A_NAME.toDatabaseSchemaName(), DUMMY_NOTARY_NAME.toDatabaseSchemaName())
|
||||
}
|
||||
@Test
|
||||
fun `issuable currencies are read in from node config`() {
|
||||
driver(DriverParameters(
|
||||
extraCordappPackagesToScan = listOf("net.corda.finance.flows"),
|
||||
notarySpecs = emptyList())) {
|
||||
val node = startNode(customOverrides = mapOf("custom" to mapOf("issuableCurrencies" to listOf("EUR", "USD")))).getOrThrow()
|
||||
val config = node.rpc.startFlow(::CashConfigDataFlow).returnValue.getOrThrow()
|
||||
assertThat(config.issuableCurrencies).containsExactly(EUR, USD)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
package net.corda.finance.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.internal.declaredField
|
||||
import net.corda.core.node.AppServiceHub
|
||||
import net.corda.core.node.services.CordaService
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.finance.CHF
|
||||
import net.corda.finance.EUR
|
||||
import net.corda.finance.GBP
|
||||
import net.corda.finance.USD
|
||||
import net.corda.finance.flows.ConfigHolder.Companion.supportedCurrencies
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.OpenOption
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.util.*
|
||||
|
||||
// TODO Until apps have access to their own config, we'll hack things by first getting the baseDirectory, read the node.conf
|
||||
// again to get our config and store it here for access by our flow
|
||||
@CordaService
|
||||
class ConfigHolder(services: AppServiceHub) : SingletonSerializeAsToken() {
|
||||
companion object {
|
||||
val supportedCurrencies = listOf(USD, GBP, CHF, EUR)
|
||||
|
||||
// TODO: ENT-2040 - After GA, the Finance app should be fully decoupled from use of internal APIs on both OS and ENT!
|
||||
private operator fun Path.div(other: String): Path = resolve(other)
|
||||
private operator fun String.div(other: String): Path = Paths.get(this) / other
|
||||
private fun Path.inputStream(vararg options: OpenOption): InputStream = Files.newInputStream(this, *options)
|
||||
private inline fun <R> Path.read(vararg options: OpenOption, block: (InputStream) -> R): R = inputStream(*options).use(block)
|
||||
}
|
||||
|
||||
val issuableCurrencies: List<Currency>
|
||||
|
||||
init {
|
||||
// Warning!! You are about to see a major hack!
|
||||
val baseDirectory = services.declaredField<Any>("serviceHub").value
|
||||
.let { it.javaClass.getMethod("getConfiguration").apply { isAccessible = true }.invoke(it) }
|
||||
.let { it.javaClass.getMethod("getBaseDirectory").apply { isAccessible = true }.invoke(it) }
|
||||
.let { it.javaClass.getMethod("toString").apply { isAccessible = true }.invoke(it) as String }
|
||||
|
||||
var issuableCurrenciesValue: List<Currency>
|
||||
try {
|
||||
val config = (baseDirectory / "node.conf").read { ConfigFactory.parseReader(it.reader()) }
|
||||
if (config.hasPath("custom.issuableCurrencies")) {
|
||||
issuableCurrenciesValue = config.getStringList("custom.issuableCurrencies").map { Currency.getInstance(it) }
|
||||
require(supportedCurrencies.containsAll(issuableCurrenciesValue))
|
||||
} else {
|
||||
issuableCurrenciesValue = emptyList()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
issuableCurrenciesValue = emptyList()
|
||||
}
|
||||
issuableCurrencies = issuableCurrenciesValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flow to obtain cash cordapp app configuration.
|
||||
*/
|
||||
@StartableByRPC
|
||||
class CashConfigDataFlow : FlowLogic<CashConfiguration>() {
|
||||
@Suspendable
|
||||
override fun call(): CashConfiguration {
|
||||
val configHolder = serviceHub.cordaService(ConfigHolder::class.java)
|
||||
return CashConfiguration(configHolder.issuableCurrencies, supportedCurrencies)
|
||||
}
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
data class CashConfiguration(val issuableCurrencies: List<Currency>, val supportedCurrencies: List<Currency>)
|
@ -0,0 +1,48 @@
|
||||
package net.corda.finance.internal
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.node.AppServiceHub
|
||||
import net.corda.core.node.services.CordaService
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.finance.CHF
|
||||
import net.corda.finance.EUR
|
||||
import net.corda.finance.GBP
|
||||
import net.corda.finance.USD
|
||||
import net.corda.finance.internal.ConfigHolder.Companion.supportedCurrencies
|
||||
import java.util.*
|
||||
|
||||
@CordaService
|
||||
class ConfigHolder(services: AppServiceHub) : SingletonSerializeAsToken() {
|
||||
companion object {
|
||||
val supportedCurrencies = listOf(USD, GBP, CHF, EUR)
|
||||
}
|
||||
|
||||
val issuableCurrencies: List<Currency>
|
||||
|
||||
init {
|
||||
val issuableCurrenciesStringList: List<String> = uncheckedCast(services.getAppContext().config.get("issuableCurrencies"))
|
||||
issuableCurrencies = issuableCurrenciesStringList.map(Currency::getInstance)
|
||||
(issuableCurrencies - supportedCurrencies).let {
|
||||
require(it.isEmpty()) { "$it are not supported currencies" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flow to obtain cash cordapp app configuration.
|
||||
*/
|
||||
@StartableByRPC
|
||||
class CashConfigDataFlow : FlowLogic<CashConfiguration>() {
|
||||
@Suspendable
|
||||
override fun call(): CashConfiguration {
|
||||
val configHolder = serviceHub.cordaService(ConfigHolder::class.java)
|
||||
return CashConfiguration(configHolder.issuableCurrencies, supportedCurrencies)
|
||||
}
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
data class CashConfiguration(val issuableCurrencies: List<Currency>, val supportedCurrencies: List<Currency>)
|
@ -0,0 +1,29 @@
|
||||
package net.corda.finance.internal
|
||||
|
||||
import net.corda.core.internal.packageName
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.EUR
|
||||
import net.corda.finance.USD
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockNetworkParameters
|
||||
import net.corda.testing.node.MockNodeParameters
|
||||
import net.corda.testing.node.internal.cordappForPackages
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Test
|
||||
|
||||
class CashConfigDataFlowTest {
|
||||
private val mockNet = MockNetwork(emptyList(), MockNetworkParameters(threadPerNode = true))
|
||||
|
||||
@After
|
||||
fun cleanUp() = mockNet.stopNodes()
|
||||
|
||||
@Test
|
||||
fun `issuable currencies read in from cordapp config`() {
|
||||
val node = mockNet.createNode(MockNodeParameters(additionalCordapps = listOf(
|
||||
cordappForPackages(javaClass.packageName).withConfig(mapOf("issuableCurrencies" to listOf("EUR", "USD")))
|
||||
)))
|
||||
val config = node.startFlow(CashConfigDataFlow()).getOrThrow()
|
||||
assertThat(config.issuableCurrencies).containsExactly(EUR, USD)
|
||||
}
|
||||
}
|
@ -99,6 +99,8 @@ const val DEV_CA_TRUST_STORE_FILE: String = "cordatruststore.jks"
|
||||
const val DEV_CA_TRUST_STORE_PASS: String = "trustpass"
|
||||
const val DEV_CA_TRUST_STORE_PRIVATE_KEY_PASS: String = "trustpasskeypass"
|
||||
|
||||
val DEV_CERTIFICATES: List<X509Certificate> get() = listOf(DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate)
|
||||
|
||||
// We need a class so that we can get hold of the class loader
|
||||
internal object DevCaHelper {
|
||||
fun loadDevCa(alias: String): CertificateAndKeyPair {
|
||||
|
@ -65,7 +65,7 @@ internal constructor(private val initSerEnv: Boolean,
|
||||
"java",
|
||||
"-jar",
|
||||
"corda.jar",
|
||||
"--just-generate-node-info"
|
||||
"generate-node-info"
|
||||
)
|
||||
|
||||
private const val LOGS_DIR_NAME = "logs"
|
||||
|
@ -82,12 +82,11 @@ class FlowCheckpointVersionNodeStartupCheckTest: IntegrationTest() {
|
||||
// Create the CorDapp jar file manually first to get hold of the directory that will contain it so that we can
|
||||
// rename the filename later. The cordappDir, which acts as pointer to the jar file, does not get renamed.
|
||||
val cordappDir = TestCordappDirectories.getJarDirectory(cordapp)
|
||||
val cordappJar = cordappDir.list().single()
|
||||
val cordappJar = cordappDir.list().single { it.toString().endsWith(".jar") }
|
||||
|
||||
createSuspendedFlowInBob(setOf(cordapp))
|
||||
|
||||
// Rename the jar file. TestCordappDirectories caches the location of the jar file but the use of the random
|
||||
// UUID in the name means there's zero chance of contaminating another test.
|
||||
// Rename the jar file.
|
||||
cordappJar.moveTo(cordappDir / "renamed-${cordappJar.fileName}")
|
||||
|
||||
assertBobFailsToStartWithLogMessage(
|
||||
@ -101,13 +100,13 @@ class FlowCheckpointVersionNodeStartupCheckTest: IntegrationTest() {
|
||||
fun `restart node with incompatible version of suspended flow due to different jar hash`() {
|
||||
driver(parametersForRestartingNodes()) {
|
||||
val originalCordapp = defaultCordapp.withName("different-jar-hash-test-${UUID.randomUUID()}")
|
||||
val originalCordappJar = TestCordappDirectories.getJarDirectory(originalCordapp).list().single()
|
||||
val originalCordappJar = TestCordappDirectories.getJarDirectory(originalCordapp).list().single { it.toString().endsWith(".jar") }
|
||||
|
||||
createSuspendedFlowInBob(setOf(originalCordapp))
|
||||
|
||||
// The vendor is part of the MANIFEST so changing it is sufficient to change the jar hash
|
||||
val modifiedCordapp = originalCordapp.withVendor("${originalCordapp.vendor}-modified")
|
||||
val modifiedCordappJar = TestCordappDirectories.getJarDirectory(modifiedCordapp).list().single()
|
||||
val modifiedCordappJar = TestCordappDirectories.getJarDirectory(modifiedCordapp).list().single { it.toString().endsWith(".jar") }
|
||||
modifiedCordappJar.moveTo(originalCordappJar, REPLACE_EXISTING)
|
||||
|
||||
assertBobFailsToStartWithLogMessage(
|
||||
|
@ -0,0 +1,85 @@
|
||||
package net.corda.node.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.node.internal.cordappForClasses
|
||||
import org.hamcrest.CoreMatchers.`is`
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
class FlowOverrideTests {
|
||||
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class Ping(private val pongParty: Party) : FlowLogic<String>() {
|
||||
@Suspendable
|
||||
override fun call(): String {
|
||||
val pongSession = initiateFlow(pongParty)
|
||||
return pongSession.sendAndReceive<String>("PING").unwrap { it }
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(Ping::class)
|
||||
open class Pong(private val pingSession: FlowSession) : FlowLogic<Unit>() {
|
||||
companion object {
|
||||
val PONG = "PONG"
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
pingSession.send(PONG)
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(Ping::class)
|
||||
class Pong2(private val pingSession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
pingSession.send("PONGPONG")
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(Ping::class)
|
||||
class Pongiest(private val pingSession: FlowSession) : Pong(pingSession) {
|
||||
|
||||
companion object {
|
||||
val GORGONZOLA = "Gorgonzola"
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
pingSession.send(GORGONZOLA)
|
||||
}
|
||||
}
|
||||
|
||||
private val nodeAClasses = setOf(Ping::class.java,
|
||||
Pong::class.java, Pongiest::class.java)
|
||||
private val nodeBClasses = setOf(Ping::class.java, Pong::class.java)
|
||||
|
||||
@Test
|
||||
fun `should use the most specific implementation of a responding flow`() {
|
||||
driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = emptySet())) {
|
||||
val nodeA = startNode(additionalCordapps = setOf(cordappForClasses(*nodeAClasses.toTypedArray()))).getOrThrow()
|
||||
val nodeB = startNode(additionalCordapps = setOf(cordappForClasses(*nodeBClasses.toTypedArray()))).getOrThrow()
|
||||
Assert.assertThat(nodeB.rpc.startFlow(::Ping, nodeA.nodeInfo.singleIdentity()).returnValue.getOrThrow(), `is`(net.corda.node.flows.FlowOverrideTests.Pongiest.GORGONZOLA))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should use the overriden implementation of a responding flow`() {
|
||||
val flowOverrides = mapOf(Ping::class.java to Pong::class.java)
|
||||
driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = emptySet())) {
|
||||
val nodeA = startNode(additionalCordapps = setOf(cordappForClasses(*nodeAClasses.toTypedArray())), flowOverrides = flowOverrides).getOrThrow()
|
||||
val nodeB = startNode(additionalCordapps = setOf(cordappForClasses(*nodeBClasses.toTypedArray()))).getOrThrow()
|
||||
Assert.assertThat(nodeB.rpc.startFlow(::Ping, nodeA.nodeInfo.singleIdentity()).returnValue.getOrThrow(), `is`(net.corda.node.flows.FlowOverrideTests.Pong.PONG))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -7,12 +7,13 @@ import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.testing.internal.IntegrationTestSchemas
|
||||
import net.corda.testing.internal.toDatabaseSchemaName
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.internal.NodeBasedTest
|
||||
import net.corda.node.internal.NodeFlowManager
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.singleIdentity
|
||||
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.startFlow
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.ClassRule
|
||||
@ -27,9 +28,10 @@ class FlowVersioningTest : NodeBasedTest() {
|
||||
|
||||
@Test
|
||||
fun `getFlowContext returns the platform version for core flows`() {
|
||||
val bobFlowManager = NodeFlowManager()
|
||||
val alice = startNode(ALICE_NAME, platformVersion = 2)
|
||||
val bob = startNode(BOB_NAME, platformVersion = 3)
|
||||
bob.node.installCoreFlow(PretendInitiatingCoreFlow::class, ::PretendInitiatedCoreFlow)
|
||||
val bob = startNode(BOB_NAME, platformVersion = 3, flowManager = bobFlowManager)
|
||||
bobFlowManager.registerInitiatedCoreFlowFactory(PretendInitiatingCoreFlow::class, ::PretendInitiatedCoreFlow)
|
||||
val (alicePlatformVersionAccordingToBob, bobPlatformVersionAccordingToAlice) = alice.services.startFlow(
|
||||
PretendInitiatingCoreFlow(bob.info.singleIdentity())).resultFuture.getOrThrow()
|
||||
assertThat(alicePlatformVersionAccordingToBob).isEqualTo(2)
|
||||
@ -54,4 +56,5 @@ class FlowVersioningTest : NodeBasedTest() {
|
||||
@Suspendable
|
||||
override fun call() = otherSideSession.send(otherSideSession.getCounterpartyFlowInfo().flowVersion)
|
||||
}
|
||||
|
||||
}
|
@ -10,5 +10,5 @@ fun main(args: Array<String>) {
|
||||
// Pass the arguments to the Node factory. In the Enterprise edition, this line is modified to point to a subclass.
|
||||
// It will exit the process in case of startup failure and is not intended to be used by embedders. If you want
|
||||
// to embed Node in your own container, instantiate it directly and set up the configuration objects yourself.
|
||||
EnterpriseNode.Startup().start(args)
|
||||
EnterpriseNode.NodeCli().start(args)
|
||||
}
|
||||
|
@ -2,18 +2,18 @@ package net.corda.node
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigRenderOptions
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.node.services.config.ConfigHelper
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.NodeConfigurationImpl
|
||||
import net.corda.node.services.config.parseAsNodeConfiguration
|
||||
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
|
||||
import picocli.CommandLine.Option
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
class NodeCmdLineOptions {
|
||||
open class SharedNodeCmdLineOptions {
|
||||
@Option(
|
||||
names = ["-b", "--base-directory"],
|
||||
description = ["The node working directory where all the files are kept."]
|
||||
@ -27,6 +27,53 @@ class NodeCmdLineOptions {
|
||||
private var _configFile: Path? = null
|
||||
val configFile: Path get() = _configFile ?: (baseDirectory / "node.conf")
|
||||
|
||||
@Option(
|
||||
names = ["--on-unknown-config-keys"],
|
||||
description = ["How to behave on unknown node configuration. \${COMPLETION-CANDIDATES}"]
|
||||
)
|
||||
var unknownConfigKeysPolicy: UnknownConfigKeysPolicy = UnknownConfigKeysPolicy.FAIL
|
||||
|
||||
@Option(
|
||||
names = ["-d", "--dev-mode"],
|
||||
description = ["Runs the node in development mode. Unsafe for production."]
|
||||
)
|
||||
var devMode: Boolean? = null
|
||||
|
||||
open fun loadConfig(): NodeConfiguration {
|
||||
return getRawConfig().parseAsNodeConfiguration(unknownConfigKeysPolicy::handle)
|
||||
}
|
||||
|
||||
protected fun getRawConfig(): Config {
|
||||
val rawConfig = ConfigHelper.loadConfig(
|
||||
baseDirectory,
|
||||
configFile
|
||||
)
|
||||
if (devMode == true) {
|
||||
println("Config:\n${rawConfig.root().render(ConfigRenderOptions.defaults())}")
|
||||
}
|
||||
return rawConfig
|
||||
}
|
||||
|
||||
fun copyFrom(other: SharedNodeCmdLineOptions) {
|
||||
baseDirectory = other.baseDirectory
|
||||
_configFile = other._configFile
|
||||
unknownConfigKeysPolicy= other.unknownConfigKeysPolicy
|
||||
devMode = other.devMode
|
||||
}
|
||||
}
|
||||
|
||||
class InitialRegistrationCmdLineOptions : SharedNodeCmdLineOptions() {
|
||||
override fun loadConfig(): NodeConfiguration {
|
||||
return getRawConfig().parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config ->
|
||||
require(!config.devMode) { "Registration cannot occur in development mode" }
|
||||
require(config.compatibilityZoneURL != null || config.networkServices != null) {
|
||||
"compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class NodeCmdLineOptions : SharedNodeCmdLineOptions() {
|
||||
@Option(
|
||||
names = ["--sshd"],
|
||||
description = ["If set, enables SSH server for node administration."]
|
||||
@ -45,84 +92,66 @@ class NodeCmdLineOptions {
|
||||
)
|
||||
var noLocalShell: Boolean = false
|
||||
|
||||
@Option(
|
||||
names = ["--initial-registration"],
|
||||
description = ["Start initial node registration with Corda network to obtain certificate from the permissioning server."]
|
||||
)
|
||||
var isRegistration: Boolean = false
|
||||
|
||||
@Option(
|
||||
names = ["-t", "--network-root-truststore"],
|
||||
description = ["Network root trust store obtained from network operator."]
|
||||
)
|
||||
private var _networkRootTrustStorePath: Path? = null
|
||||
val networkRootTrustStorePath: Path get() = _networkRootTrustStorePath ?: baseDirectory / "certificates" / "network-root-truststore.jks"
|
||||
|
||||
@Option(
|
||||
names = ["-p", "--network-root-truststore-password"],
|
||||
description = ["Network root trust store password obtained from network operator."]
|
||||
)
|
||||
var networkRootTrustStorePassword: String? = null
|
||||
|
||||
@Option(
|
||||
names = ["--on-unknown-config-keys"],
|
||||
description = ["How to behave on unknown node configuration. \${COMPLETION-CANDIDATES}"]
|
||||
)
|
||||
var unknownConfigKeysPolicy: UnknownConfigKeysPolicy = UnknownConfigKeysPolicy.FAIL
|
||||
|
||||
@Option(
|
||||
names = ["-d", "--dev-mode"],
|
||||
description = ["Run the node in developer mode. Unsafe for production."]
|
||||
)
|
||||
var devMode: Boolean? = null
|
||||
|
||||
@Option(
|
||||
names = ["--just-generate-node-info"],
|
||||
description = ["Perform the node start-up task necessary to generate its node info, save it to disk, then quit"]
|
||||
description = ["DEPRECATED. Performs the node start-up tasks necessary to generate the nodeInfo file, saves it to disk, then exits."],
|
||||
hidden = true
|
||||
)
|
||||
var justGenerateNodeInfo: Boolean = false
|
||||
|
||||
@Option(
|
||||
names = ["--just-generate-rpc-ssl-settings"],
|
||||
description = ["Generate the SSL key and trust stores for a secure RPC connection."]
|
||||
description = ["DEPRECATED. Generates the SSL key and trust stores for a secure RPC connection."],
|
||||
hidden = true
|
||||
)
|
||||
var justGenerateRpcSslCerts: Boolean = false
|
||||
|
||||
@Option(
|
||||
names = ["-c", "--clear-network-map-cache"],
|
||||
description = ["Clears local copy of network map, on node startup it will be restored from server or file system."]
|
||||
names = ["--clear-network-map-cache"],
|
||||
description = ["DEPRECATED. Clears local copy of network map, on node startup it will be restored from server or file system."],
|
||||
hidden = true
|
||||
)
|
||||
var clearNetworkMapCache: Boolean = false
|
||||
|
||||
val nodeRegistrationOption: NodeRegistrationOption? by lazy {
|
||||
if (isRegistration) {
|
||||
requireNotNull(networkRootTrustStorePassword) { "Network root trust store password must be provided in registration mode using --network-root-truststore-password." }
|
||||
require(networkRootTrustStorePath.exists()) { "Network root trust store path: '$networkRootTrustStorePath' doesn't exist" }
|
||||
NodeRegistrationOption(networkRootTrustStorePath, networkRootTrustStorePassword!!)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
@Option(
|
||||
names = ["--initial-registration"],
|
||||
description = ["DEPRECATED. Starts initial node registration with Corda network to obtain certificate from the permissioning server."],
|
||||
hidden = true
|
||||
)
|
||||
var isRegistration: Boolean = false
|
||||
|
||||
fun loadConfig(): Pair<Config, Try<NodeConfiguration>> {
|
||||
@Option(
|
||||
names = ["-t", "--network-root-truststore"],
|
||||
description = ["DEPRECATED. Network root trust store obtained from network operator."],
|
||||
hidden = true
|
||||
)
|
||||
var networkRootTrustStorePathParameter: Path? = null
|
||||
|
||||
@Option(
|
||||
names = ["-p", "--network-root-truststore-password"],
|
||||
description = ["DEPRECATED. Network root trust store password obtained from network operator."],
|
||||
hidden = true
|
||||
)
|
||||
var networkRootTrustStorePassword: String? = null
|
||||
|
||||
override fun loadConfig(): NodeConfiguration {
|
||||
val rawConfig = ConfigHelper.loadConfig(
|
||||
baseDirectory,
|
||||
configFile,
|
||||
configOverrides = ConfigFactory.parseMap(mapOf("noLocalShell" to this.noLocalShell) +
|
||||
if (sshdServer) mapOf("sshd" to mapOf("port" to sshdServerPort.toString())) else emptyMap<String, Any>() +
|
||||
if (devMode != null) mapOf("devMode" to this.devMode) else emptyMap())
|
||||
if (devMode != null) mapOf("devMode" to this.devMode) else emptyMap())
|
||||
)
|
||||
return rawConfig to Try.on {
|
||||
rawConfig.parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config ->
|
||||
if (nodeRegistrationOption != null) {
|
||||
require(!config.devMode) { "Registration cannot occur in devMode" }
|
||||
require(config.compatibilityZoneURL != null || config.networkServices != null) {
|
||||
"compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode."
|
||||
}
|
||||
return rawConfig.parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config ->
|
||||
if (isRegistration) {
|
||||
require(!config.devMode) { "Registration cannot occur in development mode" }
|
||||
require(config.compatibilityZoneURL != null || config.networkServices != null) {
|
||||
"compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class NodeRegistrationOption(val networkRootTrustStorePath: Path, val networkRootTrustStorePassword: String)
|
||||
|
@ -31,7 +31,10 @@ import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.serialization.SerializationWhitelist
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.minutes
|
||||
import net.corda.node.CordaClock
|
||||
import net.corda.node.SerialFilter
|
||||
import net.corda.node.VersionInfo
|
||||
@ -70,6 +73,7 @@ import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.node.services.upgrade.ContractUpgradeServiceImpl
|
||||
import net.corda.node.services.vault.NodeVaultService
|
||||
import net.corda.node.utilities.*
|
||||
import net.corda.nodeapi.internal.DEV_CERTIFICATES
|
||||
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
import net.corda.nodeapi.internal.config.CertificateStore
|
||||
@ -102,13 +106,10 @@ import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.time.format.DateTimeParseException
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit.MINUTES
|
||||
import java.util.concurrent.TimeUnit.SECONDS
|
||||
import kotlin.collections.set
|
||||
import kotlin.reflect.KClass
|
||||
import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair
|
||||
|
||||
/**
|
||||
@ -123,11 +124,11 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
val platformClock: CordaClock,
|
||||
cacheFactoryPrototype: BindableNamedCacheFactory,
|
||||
protected val versionInfo: VersionInfo,
|
||||
protected val flowManager: FlowManager,
|
||||
protected val serverThread: AffinityExecutor.ServiceAffinityExecutor,
|
||||
protected val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() {
|
||||
|
||||
protected abstract val log: Logger
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
private var tokenizableServices: MutableList<Any>? = mutableListOf(platformClock, this)
|
||||
|
||||
@ -169,7 +170,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
val transactionStorage = makeTransactionStorage(configuration.transactionCacheSizeBytes).tokenize()
|
||||
val networkMapClient: NetworkMapClient? = configuration.networkServices?.let { NetworkMapClient(it.networkMapURL, versionInfo) }
|
||||
val attachments = NodeAttachmentService(metricRegistry, cacheFactory, database).tokenize()
|
||||
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments).tokenize()
|
||||
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(configuration.cordappDirectories), attachments).tokenize()
|
||||
@Suppress("LeakingThis")
|
||||
val keyManagementService = makeKeyManagementService(identityService).tokenize()
|
||||
val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, transactionStorage).also {
|
||||
@ -213,7 +214,6 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
).tokenize().closeOnStop()
|
||||
|
||||
private val cordappServices = MutableClassToInstanceMap.create<SerializeAsToken>()
|
||||
private val flowFactories = ConcurrentHashMap<Class<out FlowLogic<*>>, InitiatedFlowFactory<*>>()
|
||||
private val shutdownExecutor = Executors.newSingleThreadExecutor()
|
||||
|
||||
protected abstract val transactionVerifierWorkerCount: Int
|
||||
@ -239,7 +239,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
private var _started: S? = null
|
||||
|
||||
private fun <T : Any> T.tokenize(): T {
|
||||
tokenizableServices?.add(this) ?: throw IllegalStateException("The tokenisable services list has already been finalised")
|
||||
tokenizableServices?.add(this)
|
||||
?: throw IllegalStateException("The tokenisable services list has already been finalised")
|
||||
return this
|
||||
}
|
||||
|
||||
@ -530,10 +531,12 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
// CorDapp will be generated.
|
||||
generatedCordapps += VirtualCordapp.generateSimpleNotaryCordapp(versionInfo)
|
||||
}
|
||||
val blacklistedCerts = if (configuration.devMode) emptyList() else DEV_CERTIFICATES
|
||||
return JarScanningCordappLoader.fromDirectories(
|
||||
configuration.cordappDirectories,
|
||||
versionInfo,
|
||||
extraCordapps = generatedCordapps
|
||||
extraCordapps = generatedCordapps,
|
||||
blacklistedCerts = blacklistedCerts
|
||||
)
|
||||
}
|
||||
|
||||
@ -624,91 +627,27 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
}
|
||||
|
||||
private fun registerCordappFlows() {
|
||||
cordappLoader.cordapps.flatMap { it.initiatedFlows }
|
||||
.forEach {
|
||||
cordappLoader.cordapps.forEach { cordapp ->
|
||||
cordapp.initiatedFlows.groupBy { it.requireAnnotation<InitiatedBy>().value.java }.forEach { initiator, responders ->
|
||||
responders.forEach { responder ->
|
||||
try {
|
||||
registerInitiatedFlowInternal(smm, it, track = false)
|
||||
flowManager.registerInitiatedFlow(initiator, responder)
|
||||
} catch (e: NoSuchMethodException) {
|
||||
log.error("${it.name}, as an initiated flow, must have a constructor with a single parameter " +
|
||||
log.error("${responder.name}, as an initiated flow, must have a constructor with a single parameter " +
|
||||
"of type ${Party::class.java.name}")
|
||||
} catch (e: Exception) {
|
||||
log.error("Unable to register initiated flow ${it.name}", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : FlowLogic<*>> registerInitiatedFlow(smm: StateMachineManager, initiatedFlowClass: Class<T>): Observable<T> {
|
||||
return registerInitiatedFlowInternal(smm, initiatedFlowClass, track = true)
|
||||
}
|
||||
|
||||
// TODO remove once not needed
|
||||
private fun deprecatedFlowConstructorMessage(flowClass: Class<*>): String {
|
||||
return "Installing flow factory for $flowClass accepting a ${Party::class.java.simpleName}, which is deprecated. " +
|
||||
"It should accept a ${FlowSession::class.java.simpleName} instead"
|
||||
}
|
||||
|
||||
private fun <F : FlowLogic<*>> registerInitiatedFlowInternal(smm: StateMachineManager, initiatedFlow: Class<F>, track: Boolean): Observable<F> {
|
||||
val constructors = initiatedFlow.declaredConstructors.associateBy { it.parameterTypes.toList() }
|
||||
val flowSessionCtor = constructors[listOf(FlowSession::class.java)]?.apply { isAccessible = true }
|
||||
val ctor: (FlowSession) -> F = if (flowSessionCtor == null) {
|
||||
// Try to fallback to a Party constructor
|
||||
val partyCtor = constructors[listOf(Party::class.java)]?.apply { isAccessible = true }
|
||||
if (partyCtor == null) {
|
||||
throw IllegalArgumentException("$initiatedFlow must have a constructor accepting a ${FlowSession::class.java.name}")
|
||||
} else {
|
||||
log.warn(deprecatedFlowConstructorMessage(initiatedFlow))
|
||||
}
|
||||
{ flowSession: FlowSession -> uncheckedCast(partyCtor.newInstance(flowSession.counterparty)) }
|
||||
} else {
|
||||
{ flowSession: FlowSession -> uncheckedCast(flowSessionCtor.newInstance(flowSession)) }
|
||||
}
|
||||
val initiatingFlow = initiatedFlow.requireAnnotation<InitiatedBy>().value.java
|
||||
val (version, classWithAnnotation) = initiatingFlow.flowVersionAndInitiatingClass
|
||||
require(classWithAnnotation == initiatingFlow) {
|
||||
"${InitiatedBy::class.java.name} must point to ${classWithAnnotation.name} and not ${initiatingFlow.name}"
|
||||
}
|
||||
val flowFactory = InitiatedFlowFactory.CorDapp(version, initiatedFlow.appName, ctor)
|
||||
val observable = internalRegisterFlowFactory(smm, initiatingFlow, flowFactory, initiatedFlow, track)
|
||||
log.info("Registered ${initiatingFlow.name} to initiate ${initiatedFlow.name} (version $version)")
|
||||
return observable
|
||||
}
|
||||
|
||||
protected fun <F : FlowLogic<*>> internalRegisterFlowFactory(smm: StateMachineManager,
|
||||
initiatingFlowClass: Class<out FlowLogic<*>>,
|
||||
flowFactory: InitiatedFlowFactory<F>,
|
||||
initiatedFlowClass: Class<F>,
|
||||
track: Boolean): Observable<F> {
|
||||
val observable = if (track) {
|
||||
smm.changes.filter { it is StateMachineManager.Change.Add }.map { it.logic }.ofType(initiatedFlowClass)
|
||||
} else {
|
||||
Observable.empty()
|
||||
}
|
||||
check(initiatingFlowClass !in flowFactories.keys) {
|
||||
"$initiatingFlowClass is attempting to register multiple initiated flows"
|
||||
}
|
||||
flowFactories[initiatingFlowClass] = flowFactory
|
||||
return observable
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs a flow that's core to the Corda platform. Unlike CorDapp flows which are versioned individually using
|
||||
* [InitiatingFlow.version], core flows have the same version as the node's platform version. To cater for backwards
|
||||
* compatibility [flowFactory] provides a second parameter which is the platform version of the initiating party.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
fun installCoreFlow(clientFlowClass: KClass<out FlowLogic<*>>, flowFactory: (FlowSession) -> FlowLogic<*>) {
|
||||
require(clientFlowClass.java.flowVersionAndInitiatingClass.first == 1) {
|
||||
"${InitiatingFlow::class.java.name}.version not applicable for core flows; their version is the node's platform version"
|
||||
}
|
||||
flowFactories[clientFlowClass.java] = InitiatedFlowFactory.Core(flowFactory)
|
||||
log.debug { "Installed core flow ${clientFlowClass.java.name}" }
|
||||
flowManager.validateRegistrations()
|
||||
}
|
||||
|
||||
private fun installCoreFlows() {
|
||||
installCoreFlow(FinalityFlow::class, ::FinalityHandler)
|
||||
installCoreFlow(NotaryChangeFlow::class, ::NotaryChangeHandler)
|
||||
installCoreFlow(ContractUpgradeFlow.Initiate::class, ::ContractUpgradeHandler)
|
||||
installCoreFlow(SwapIdentitiesFlow::class, ::SwapIdentitiesHandler)
|
||||
flowManager.registerInitiatedCoreFlowFactory(FinalityFlow::class, FinalityHandler::class, ::FinalityHandler)
|
||||
flowManager.registerInitiatedCoreFlowFactory(NotaryChangeFlow::class, NotaryChangeHandler::class, ::NotaryChangeHandler)
|
||||
flowManager.registerInitiatedCoreFlowFactory(ContractUpgradeFlow.Initiate::class, NotaryChangeHandler::class, ::ContractUpgradeHandler)
|
||||
flowManager.registerInitiatedCoreFlowFactory(SwapIdentitiesFlow::class, SwapIdentitiesHandler::class, ::SwapIdentitiesHandler)
|
||||
}
|
||||
|
||||
protected open fun makeTransactionStorage(transactionCacheSizeBytes: Long): WritableTransactionStorage {
|
||||
@ -739,7 +678,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
requireNotNull(getCertificateStores()) {
|
||||
"One or more keyStores (identity or TLS) or trustStore not found. " +
|
||||
"Please either copy your existing keys and certificates from another node, " +
|
||||
"or if you don't have one yet, fill out the config file and run corda.jar --initial-registration. " +
|
||||
"or if you don't have one yet, fill out the config file and run corda.jar initial-registration. " +
|
||||
"Read more at: https://docs.corda.net/permissioning.html"
|
||||
}
|
||||
} catch (e: KeyStoreException) {
|
||||
@ -811,7 +750,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
service.run {
|
||||
tokenize()
|
||||
runOnStop += ::stop
|
||||
installCoreFlow(NotaryFlow.Client::class, ::createServiceFlow)
|
||||
flowManager.registerInitiatedCoreFlowFactory(NotaryFlow.Client::class, ::createServiceFlow)
|
||||
start()
|
||||
}
|
||||
return service
|
||||
@ -998,7 +937,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
}
|
||||
|
||||
override fun getFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>): InitiatedFlowFactory<*>? {
|
||||
return flowFactories[initiatingFlowClass]
|
||||
return flowManager.getFlowFactoryForInitiatingFlow(initiatingFlowClass)
|
||||
}
|
||||
|
||||
override fun jdbcSession(): Connection = database.createSession()
|
||||
@ -1103,4 +1042,4 @@ fun clientSslOptionsCompatibleWith(nodeRpcOptions: NodeRpcOptions): ClientRpcSsl
|
||||
}
|
||||
// Here we're using the node's RPC key store as the RPC client's trust store.
|
||||
return ClientRpcSslOptions(trustStorePath = nodeRpcOptions.sslConfig!!.keyStorePath, trustStorePassword = nodeRpcOptions.sslConfig!!.keyStorePassword)
|
||||
}
|
||||
}
|
@ -29,8 +29,9 @@ import java.util.concurrent.TimeUnit
|
||||
|
||||
open class EnterpriseNode(configuration: NodeConfiguration,
|
||||
versionInfo: VersionInfo,
|
||||
initialiseSerialization: Boolean = true
|
||||
) : Node(configuration, versionInfo, initialiseSerialization, cacheFactoryPrototype = EnterpriseNamedCacheFactory(configuration.enterpriseConfiguration.getTracingConfig())) {
|
||||
initialiseSerialization: Boolean = true,
|
||||
flowManager: FlowManager = NodeFlowManager(configuration.flowOverrides)
|
||||
) : Node(configuration, versionInfo, initialiseSerialization, flowManager, cacheFactoryPrototype = EnterpriseNamedCacheFactory(configuration.enterpriseConfiguration.getTracingConfig())) {
|
||||
companion object {
|
||||
private val logger by lazy { loggerFor<EnterpriseNode>() }
|
||||
|
||||
@ -50,6 +51,10 @@ open class EnterpriseNode(configuration: NodeConfiguration,
|
||||
}
|
||||
}
|
||||
|
||||
class NodeCli : NodeStartupCli() {
|
||||
override val startup = Startup()
|
||||
}
|
||||
|
||||
class Startup : NodeStartup() {
|
||||
override fun preNetworkRegistration(conf: NodeConfiguration) {
|
||||
super.preNetworkRegistration(conf)
|
||||
|
222
node/src/main/kotlin/net/corda/node/internal/FlowManager.kt
Normal file
222
node/src/main/kotlin/net/corda/node/internal/FlowManager.kt
Normal file
@ -0,0 +1,222 @@
|
||||
package net.corda.node.internal
|
||||
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.node.internal.classloading.requireAnnotation
|
||||
import net.corda.node.services.config.FlowOverrideConfig
|
||||
import net.corda.node.services.statemachine.appName
|
||||
import net.corda.node.services.statemachine.flowVersionAndInitiatingClass
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
*
|
||||
* This class is responsible for organising which flow should respond to a specific @InitiatingFlow
|
||||
*
|
||||
* There are two main ways to modify the behaviour of a cordapp with regards to responding with a different flow
|
||||
*
|
||||
* 1.) implementing a new subclass. For example, if we have a ResponderFlow similar to @InitiatedBy(Sender) MyBaseResponder : FlowLogic
|
||||
* If we subclassed a new Flow with specific logic for DB2, it would be similar to IBMB2Responder() : MyBaseResponder
|
||||
* When these two flows are encountered by the classpath scan for @InitiatedBy, they will both be selected for responding to Sender
|
||||
* This implementation will sort them for responding in order of their "depth" from FlowLogic - see: FlowWeightComparator
|
||||
* So IBMB2Responder would win and it would be selected for responding
|
||||
*
|
||||
* 2.) It is possible to specify a flowOverride key in the node configuration. Say we configure a node to have
|
||||
* flowOverrides{
|
||||
* "Sender" = "MyBaseResponder"
|
||||
* }
|
||||
* In this case, FlowWeightComparator would detect that there is an override in action, and it will assign MyBaseResponder a maximum weight
|
||||
* This will result in MyBaseResponder being selected for responding to Sender
|
||||
*
|
||||
*
|
||||
*/
|
||||
interface FlowManager {
|
||||
|
||||
fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass<out FlowLogic<*>>, flowFactory: (FlowSession) -> FlowLogic<*>)
|
||||
fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass<out FlowLogic<*>>, initiatedFlowClass: KClass<out FlowLogic<*>>?, flowFactory: (FlowSession) -> FlowLogic<*>)
|
||||
fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass<out FlowLogic<*>>, initiatedFlowClass: KClass<out FlowLogic<*>>?, flowFactory: InitiatedFlowFactory.Core<FlowLogic<*>>)
|
||||
|
||||
fun <F : FlowLogic<*>> registerInitiatedFlow(initiator: Class<out FlowLogic<*>>, responder: Class<F>)
|
||||
fun <F : FlowLogic<*>> registerInitiatedFlow(responder: Class<F>)
|
||||
|
||||
fun getFlowFactoryForInitiatingFlow(initiatedFlowClass: Class<out FlowLogic<*>>): InitiatedFlowFactory<*>?
|
||||
|
||||
fun validateRegistrations()
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
open class NodeFlowManager(flowOverrides: FlowOverrideConfig? = null) : FlowManager {
|
||||
|
||||
private val flowFactories = HashMap<Class<out FlowLogic<*>>, MutableList<RegisteredFlowContainer>>()
|
||||
private val flowOverrides = (flowOverrides
|
||||
?: FlowOverrideConfig()).overrides.map { it.initiator to it.responder }.toMutableMap()
|
||||
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun getFlowFactoryForInitiatingFlow(initiatedFlowClass: Class<out FlowLogic<*>>): InitiatedFlowFactory<*>? {
|
||||
return flowFactories[initiatedFlowClass]?.firstOrNull()?.flowFactory
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun <F : FlowLogic<*>> registerInitiatedFlow(responder: Class<F>) {
|
||||
return registerInitiatedFlow(responder.requireAnnotation<InitiatedBy>().value.java, responder)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun <F : FlowLogic<*>> registerInitiatedFlow(initiator: Class<out FlowLogic<*>>, responder: Class<F>) {
|
||||
val constructors = responder.declaredConstructors.associateBy { it.parameterTypes.toList() }
|
||||
val flowSessionCtor = constructors[listOf(FlowSession::class.java)]?.apply { isAccessible = true }
|
||||
val ctor: (FlowSession) -> F = if (flowSessionCtor == null) {
|
||||
// Try to fallback to a Party constructor
|
||||
val partyCtor = constructors[listOf(Party::class.java)]?.apply { isAccessible = true }
|
||||
if (partyCtor == null) {
|
||||
throw IllegalArgumentException("$responder must have a constructor accepting a ${FlowSession::class.java.name}")
|
||||
} else {
|
||||
log.warn("Installing flow factory for $responder accepting a ${Party::class.java.simpleName}, which is deprecated. " +
|
||||
"It should accept a ${FlowSession::class.java.simpleName} instead")
|
||||
}
|
||||
{ flowSession: FlowSession -> uncheckedCast(partyCtor.newInstance(flowSession.counterparty)) }
|
||||
} else {
|
||||
{ flowSession: FlowSession -> uncheckedCast(flowSessionCtor.newInstance(flowSession)) }
|
||||
}
|
||||
val (version, classWithAnnotation) = initiator.flowVersionAndInitiatingClass
|
||||
require(classWithAnnotation == initiator) {
|
||||
"${InitiatedBy::class.java.name} must point to ${classWithAnnotation.name} and not ${initiator.name}"
|
||||
}
|
||||
val flowFactory = InitiatedFlowFactory.CorDapp(version, responder.appName, ctor)
|
||||
registerInitiatedFlowFactory(initiator, flowFactory, responder)
|
||||
log.info("Registered ${initiator.name} to initiate ${responder.name} (version $version)")
|
||||
}
|
||||
|
||||
private fun <F : FlowLogic<*>> registerInitiatedFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>,
|
||||
flowFactory: InitiatedFlowFactory<F>,
|
||||
initiatedFlowClass: Class<F>?) {
|
||||
|
||||
check(flowFactory !is InitiatedFlowFactory.Core) { "This should only be used for Cordapp flows" }
|
||||
val listOfFlowsForInitiator = flowFactories.computeIfAbsent(initiatingFlowClass) { mutableListOf() }
|
||||
if (listOfFlowsForInitiator.isNotEmpty() && listOfFlowsForInitiator.first().type == FlowType.CORE) {
|
||||
throw IllegalStateException("Attempting to register over an existing platform flow: $initiatingFlowClass")
|
||||
}
|
||||
synchronized(listOfFlowsForInitiator) {
|
||||
val flowToAdd = RegisteredFlowContainer(initiatingFlowClass, initiatedFlowClass, flowFactory, FlowType.CORDAPP)
|
||||
val flowWeightComparator = FlowWeightComparator(initiatingFlowClass, flowOverrides)
|
||||
listOfFlowsForInitiator.add(flowToAdd)
|
||||
listOfFlowsForInitiator.sortWith(flowWeightComparator)
|
||||
if (listOfFlowsForInitiator.size > 1) {
|
||||
log.warn("Multiple flows are registered for InitiatingFlow: $initiatingFlowClass, currently using: ${listOfFlowsForInitiator.first().initiatedFlowClass}")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TODO Harmonise use of these methods - 99% of invocations come from tests.
|
||||
@Synchronized
|
||||
override fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass<out FlowLogic<*>>, initiatedFlowClass: KClass<out FlowLogic<*>>?, flowFactory: (FlowSession) -> FlowLogic<*>) {
|
||||
registerInitiatedCoreFlowFactory(initiatingFlowClass, initiatedFlowClass, InitiatedFlowFactory.Core(flowFactory))
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass<out FlowLogic<*>>, flowFactory: (FlowSession) -> FlowLogic<*>) {
|
||||
registerInitiatedCoreFlowFactory(initiatingFlowClass, null, InitiatedFlowFactory.Core(flowFactory))
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass<out FlowLogic<*>>, initiatedFlowClass: KClass<out FlowLogic<*>>?, flowFactory: InitiatedFlowFactory.Core<FlowLogic<*>>) {
|
||||
require(initiatingFlowClass.java.flowVersionAndInitiatingClass.first == 1) {
|
||||
"${InitiatingFlow::class.java.name}.version not applicable for core flows; their version is the node's platform version"
|
||||
}
|
||||
flowFactories.computeIfAbsent(initiatingFlowClass.java) { mutableListOf() }.add(
|
||||
RegisteredFlowContainer(
|
||||
initiatingFlowClass.java,
|
||||
initiatedFlowClass?.java,
|
||||
flowFactory,
|
||||
FlowType.CORE)
|
||||
)
|
||||
log.debug { "Installed core flow ${initiatingFlowClass.java.name}" }
|
||||
}
|
||||
|
||||
// To verify the integrity of the current state, it is important that the tip of the responders is a unique weight
|
||||
// if there are multiple flows with the same weight as the tip, it means that it is impossible to reliably pick one as the responder
|
||||
private fun validateInvariants(toValidate: List<RegisteredFlowContainer>) {
|
||||
val currentTip = toValidate.first()
|
||||
val flowWeightComparator = FlowWeightComparator(currentTip.initiatingFlowClass, flowOverrides)
|
||||
val equalWeightAsCurrentTip = toValidate.map { flowWeightComparator.compare(currentTip, it) to it }.filter { it.first == 0 }.map { it.second }
|
||||
if (equalWeightAsCurrentTip.size > 1) {
|
||||
val message = "Unable to determine which flow to use when responding to: ${currentTip.initiatingFlowClass.canonicalName}. ${equalWeightAsCurrentTip.map { it.initiatedFlowClass!!.canonicalName }} are all registered with equal weight."
|
||||
throw IllegalStateException(message)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun validateRegistrations() {
|
||||
flowFactories.values.forEach {
|
||||
validateInvariants(it)
|
||||
}
|
||||
}
|
||||
|
||||
private enum class FlowType {
|
||||
CORE, CORDAPP
|
||||
}
|
||||
|
||||
private data class RegisteredFlowContainer(val initiatingFlowClass: Class<out FlowLogic<*>>,
|
||||
val initiatedFlowClass: Class<out FlowLogic<*>>?,
|
||||
val flowFactory: InitiatedFlowFactory<FlowLogic<*>>,
|
||||
val type: FlowType)
|
||||
|
||||
// this is used to sort the responding flows in order of "importance"
|
||||
// the logic is as follows
|
||||
// IF responder is a specific lambda (like for notary implementations / testing code) always return that responder
|
||||
// ELSE IF responder is present in the overrides list, always return that responder
|
||||
// ELSE compare responding flows by their depth from FlowLogic, always return the flow which is most specific (IE, has the most hops to FlowLogic)
|
||||
private open class FlowWeightComparator(val initiatingFlowClass: Class<out FlowLogic<*>>, val flowOverrides: Map<String, String>) : Comparator<NodeFlowManager.RegisteredFlowContainer> {
|
||||
|
||||
override fun compare(o1: NodeFlowManager.RegisteredFlowContainer, o2: NodeFlowManager.RegisteredFlowContainer): Int {
|
||||
if (o1.initiatedFlowClass == null && o2.initiatedFlowClass != null) {
|
||||
return Int.MIN_VALUE
|
||||
}
|
||||
if (o1.initiatedFlowClass != null && o2.initiatedFlowClass == null) {
|
||||
return Int.MAX_VALUE
|
||||
}
|
||||
|
||||
if (o1.initiatedFlowClass == null && o2.initiatedFlowClass == null) {
|
||||
return 0
|
||||
}
|
||||
|
||||
val hopsTo1 = calculateHopsToFlowLogic(initiatingFlowClass, o1.initiatedFlowClass!!)
|
||||
val hopsTo2 = calculateHopsToFlowLogic(initiatingFlowClass, o2.initiatedFlowClass!!)
|
||||
return hopsTo1.compareTo(hopsTo2) * -1
|
||||
}
|
||||
|
||||
private fun calculateHopsToFlowLogic(initiatingFlowClass: Class<out FlowLogic<*>>,
|
||||
initiatedFlowClass: Class<out FlowLogic<*>>): Int {
|
||||
|
||||
val overriddenClassName = flowOverrides[initiatingFlowClass.canonicalName]
|
||||
return if (overriddenClassName == initiatedFlowClass.canonicalName) {
|
||||
Int.MAX_VALUE
|
||||
} else {
|
||||
var currentClass: Class<*> = initiatedFlowClass
|
||||
var count = 0
|
||||
while (currentClass != FlowLogic::class.java) {
|
||||
currentClass = currentClass.superclass
|
||||
count++
|
||||
}
|
||||
count;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun <X, Y> Iterable<Pair<X, Y>>.toMutableMap(): MutableMap<X, Y> {
|
||||
return this.toMap(HashMap())
|
||||
}
|
@ -4,10 +4,12 @@ import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
|
||||
sealed class InitiatedFlowFactory<out F : FlowLogic<*>> {
|
||||
|
||||
protected abstract val factory: (FlowSession) -> F
|
||||
fun createFlow(initiatingFlowSession: FlowSession): F = factory(initiatingFlowSession)
|
||||
|
||||
data class Core<out F : FlowLogic<*>>(override val factory: (FlowSession) -> F) : InitiatedFlowFactory<F>()
|
||||
|
||||
data class CorDapp<out F : FlowLogic<*>>(val flowVersion: Int,
|
||||
val appName: String,
|
||||
override val factory: (FlowSession) -> F) : InitiatedFlowFactory<F>()
|
||||
|
@ -6,6 +6,7 @@ import com.codahale.metrics.MetricRegistry
|
||||
import com.palominolabs.metrics.newrelic.AllEnabledMetricAttributeFilter
|
||||
import com.palominolabs.metrics.newrelic.NewRelicReporter
|
||||
import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme
|
||||
import net.corda.cliutils.ShellConstants
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
@ -43,6 +44,7 @@ import net.corda.node.services.api.StartedNodeServices
|
||||
import net.corda.node.services.config.*
|
||||
import net.corda.node.services.messaging.*
|
||||
import net.corda.node.services.rpc.ArtemisRpcBroker
|
||||
import net.corda.node.services.statemachine.StateMachineManager
|
||||
import net.corda.node.utilities.*
|
||||
import net.corda.node.utilities.profiling.getTracingConfig
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingClient
|
||||
@ -58,7 +60,6 @@ import org.apache.commons.lang.SystemUtils
|
||||
import org.h2.jdbc.JdbcSQLException
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import rx.Observable
|
||||
import rx.Scheduler
|
||||
import rx.schedulers.Schedulers
|
||||
import java.net.BindException
|
||||
@ -74,8 +75,7 @@ import kotlin.system.exitProcess
|
||||
class NodeWithInfo(val node: Node, val info: NodeInfo) {
|
||||
val services: StartedNodeServices = object : StartedNodeServices, ServiceHubInternal by node.services, FlowStarter by node.flowStarter {}
|
||||
fun dispose() = node.stop()
|
||||
fun <T : FlowLogic<*>> registerInitiatedFlow(initiatedFlowClass: Class<T>): Observable<T> =
|
||||
node.registerInitiatedFlow(node.smm, initiatedFlowClass)
|
||||
fun <T : FlowLogic<*>> registerInitiatedFlow(initiatedFlowClass: Class<T>) = node.registerInitiatedFlow(node.smm, initiatedFlowClass)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -87,12 +87,14 @@ class NodeWithInfo(val node: Node, val info: NodeInfo) {
|
||||
open class Node(configuration: NodeConfiguration,
|
||||
versionInfo: VersionInfo,
|
||||
private val initialiseSerialization: Boolean = true,
|
||||
flowManager: FlowManager = NodeFlowManager(configuration.flowOverrides),
|
||||
cacheFactoryPrototype: BindableNamedCacheFactory = EnterpriseNamedCacheFactory(configuration.enterpriseConfiguration.getTracingConfig())
|
||||
) : AbstractNode<NodeInfo>(
|
||||
configuration,
|
||||
createClock(configuration),
|
||||
cacheFactoryPrototype,
|
||||
versionInfo,
|
||||
flowManager,
|
||||
// Under normal (non-test execution) it will always be "1"
|
||||
AffinityExecutor.ServiceAffinityExecutor("Node thread-${sameVmNodeCounter.incrementAndGet()}", 1)
|
||||
) {
|
||||
@ -111,9 +113,13 @@ open class Node(configuration: NodeConfiguration,
|
||||
LoggerFactory.getLogger(loggerName).info(msg)
|
||||
}
|
||||
|
||||
fun printInRed(message: String) {
|
||||
println("${ShellConstants.RED}$message${ShellConstants.RESET}")
|
||||
}
|
||||
|
||||
fun printWarning(message: String) {
|
||||
Emoji.renderIfSupported {
|
||||
println("${Emoji.warningSign} ATTENTION: $message")
|
||||
printInRed("${Emoji.warningSign} ATTENTION: $message")
|
||||
}
|
||||
staticLog.warn(message)
|
||||
}
|
||||
@ -133,13 +139,13 @@ open class Node(configuration: NodeConfiguration,
|
||||
// TODO: make this configurable.
|
||||
const val MAX_RPC_MESSAGE_SIZE = 10485760
|
||||
|
||||
fun isValidJavaVersion(): Boolean {
|
||||
fun isInvalidJavaVersion(): Boolean {
|
||||
if (!hasMinimumJavaVersion()) {
|
||||
println("You are using a version of Java that is not supported (${SystemUtils.JAVA_VERSION}). Please upgrade to the latest version of Java 8.")
|
||||
println("Corda will now exit...")
|
||||
return false
|
||||
return true
|
||||
}
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
private fun hasMinimumJavaVersion(): Boolean {
|
||||
@ -204,7 +210,8 @@ open class Node(configuration: NodeConfiguration,
|
||||
return P2PMessagingClient(
|
||||
config = configuration,
|
||||
versionInfo = versionInfo,
|
||||
serverAddress = configuration.messagingServerAddress ?: NetworkHostAndPort("localhost", configuration.p2pAddress.port),
|
||||
serverAddress = configuration.messagingServerAddress
|
||||
?: NetworkHostAndPort("localhost", configuration.p2pAddress.port),
|
||||
nodeExecutor = serverThread,
|
||||
database = database,
|
||||
networkMap = networkMapCache,
|
||||
@ -230,7 +237,8 @@ open class Node(configuration: NodeConfiguration,
|
||||
}
|
||||
|
||||
val messageBroker = if (!configuration.messagingServerExternal) {
|
||||
val brokerBindAddress = configuration.messagingServerAddress ?: NetworkHostAndPort("0.0.0.0", configuration.p2pAddress.port)
|
||||
val brokerBindAddress = configuration.messagingServerAddress
|
||||
?: NetworkHostAndPort("0.0.0.0", configuration.p2pAddress.port)
|
||||
ArtemisMessagingServer(configuration, brokerBindAddress, networkParameters.maxMessageSize)
|
||||
} else {
|
||||
null
|
||||
@ -468,7 +476,7 @@ open class Node(configuration: NodeConfiguration,
|
||||
}.build().start()
|
||||
}
|
||||
|
||||
private fun registerNewRelicReporter (registry: MetricRegistry) {
|
||||
private fun registerNewRelicReporter(registry: MetricRegistry) {
|
||||
log.info("Registering New Relic JMX Reporter:")
|
||||
val reporter = NewRelicReporter.forRegistry(registry)
|
||||
.name("New Relic Reporter")
|
||||
@ -530,4 +538,8 @@ open class Node(configuration: NodeConfiguration,
|
||||
|
||||
log.info("Shutdown complete")
|
||||
}
|
||||
|
||||
fun <T : FlowLogic<*>> registerInitiatedFlow(smm: StateMachineManager, initiatedFlowClass: Class<T>) {
|
||||
this.flowManager.registerInitiatedFlow(initiatedFlowClass)
|
||||
}
|
||||
}
|
||||
|
@ -1,31 +1,24 @@
|
||||
package net.corda.node.internal
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigException
|
||||
import com.typesafe.config.ConfigRenderOptions
|
||||
import io.netty.channel.unix.Errors
|
||||
import net.corda.cliutils.CordaCliWrapper
|
||||
import net.corda.cliutils.CordaVersionProvider
|
||||
import net.corda.cliutils.ExitCodes
|
||||
import net.corda.cliutils.*
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.concurrent.thenMatch
|
||||
import net.corda.core.internal.cordapp.CordappImpl
|
||||
import net.corda.core.internal.errors.AddressBindingException
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.*
|
||||
import net.corda.node.internal.Node.Companion.isValidJavaVersion
|
||||
import net.corda.node.internal.Node.Companion.isInvalidJavaVersion
|
||||
import net.corda.node.internal.cordapp.MultipleCordappsForFlowException
|
||||
import net.corda.node.internal.subcommands.*
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.shouldStartLocalShell
|
||||
import net.corda.node.services.config.shouldStartSSHDaemon
|
||||
import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate
|
||||
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
|
||||
import net.corda.node.utilities.registration.NodeRegistrationException
|
||||
import net.corda.node.utilities.registration.NodeRegistrationHelper
|
||||
import net.corda.node.utilities.saveToKeyStore
|
||||
import net.corda.node.utilities.saveToTrustStore
|
||||
import net.corda.nodeapi.internal.addShutdownHook
|
||||
import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException
|
||||
import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException
|
||||
@ -35,10 +28,8 @@ import net.corda.nodeapi.internal.persistence.oracleJdbcDriverSerialFilter
|
||||
import net.corda.tools.shell.InteractiveShell
|
||||
import org.fusesource.jansi.Ansi
|
||||
import org.slf4j.bridge.SLF4JBridgeHandler
|
||||
import picocli.CommandLine.Mixin
|
||||
import picocli.CommandLine.*
|
||||
import sun.misc.VMSupport
|
||||
import java.io.Console
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.RandomAccessFile
|
||||
import java.lang.management.ManagementFactory
|
||||
@ -47,201 +38,146 @@ import java.nio.file.Path
|
||||
import java.time.DayOfWeek
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.*
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/** This class is responsible for starting a Node from command line arguments. */
|
||||
open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
|
||||
/** An interface that can be implemented to tell the node what to do once it's intitiated. */
|
||||
interface RunAfterNodeInitialisation {
|
||||
fun run(node: Node)
|
||||
}
|
||||
|
||||
/** Base class for subcommands to derive from that initialises the logs and provides standard options. */
|
||||
abstract class NodeCliCommand(alias: String, description: String, val startup: NodeStartup) : CliWrapperBase(alias, description), NodeStartupLogging {
|
||||
companion object {
|
||||
private val logger by lazy { loggerFor<Node>() } // I guess this is lazy to allow for logging init, but why Node?
|
||||
const val LOGS_DIRECTORY_NAME = "logs"
|
||||
const val LOGS_CAN_BE_FOUND_IN_STRING = "Logs can be found in"
|
||||
private const val INITIAL_REGISTRATION_MARKER = ".initialregistration"
|
||||
}
|
||||
|
||||
override fun initLogging() = this.initLogging(cmdLineOptions.baseDirectory)
|
||||
|
||||
@Mixin
|
||||
val cmdLineOptions = SharedNodeCmdLineOptions()
|
||||
}
|
||||
|
||||
/** Main corda entry point. */
|
||||
open class NodeStartupCli : CordaCliWrapper("corda", "Runs a Corda Node") {
|
||||
open val startup = NodeStartup()
|
||||
|
||||
private val networkCacheCli = ClearNetworkCacheCli(startup)
|
||||
private val justGenerateNodeInfoCli = GenerateNodeInfoCli(startup)
|
||||
private val justGenerateRpcSslCertsCli = GenerateRpcSslCertsCli(startup)
|
||||
private val initialRegistrationCli = InitialRegistrationCli(startup)
|
||||
|
||||
override fun initLogging() = this.initLogging(cmdLineOptions.baseDirectory)
|
||||
|
||||
override fun additionalSubCommands() = setOf(networkCacheCli, justGenerateNodeInfoCli, justGenerateRpcSslCertsCli, initialRegistrationCli)
|
||||
|
||||
override fun runProgram(): Int {
|
||||
return when {
|
||||
InitialRegistration.checkRegistrationMode(cmdLineOptions.baseDirectory) -> {
|
||||
println("Node was started before in `initial-registration` mode, but the registration was not completed.\nResuming registration.")
|
||||
initialRegistrationCli.cmdLineOptions.copyFrom(cmdLineOptions)
|
||||
initialRegistrationCli.runProgram()
|
||||
}
|
||||
//deal with legacy flags and redirect to subcommands
|
||||
cmdLineOptions.isRegistration -> {
|
||||
Node.printWarning("The --initial-registration flag has been deprecated and will be removed in a future version. Use the initial-registration command instead.")
|
||||
requireNotNull(cmdLineOptions.networkRootTrustStorePassword) { "Network root trust store password must be provided in registration mode using --network-root-truststore-password." }
|
||||
initialRegistrationCli.networkRootTrustStorePassword = cmdLineOptions.networkRootTrustStorePassword!!
|
||||
initialRegistrationCli.networkRootTrustStorePathParameter = cmdLineOptions.networkRootTrustStorePathParameter
|
||||
initialRegistrationCli.cmdLineOptions.copyFrom(cmdLineOptions)
|
||||
initialRegistrationCli.runProgram()
|
||||
}
|
||||
cmdLineOptions.clearNetworkMapCache -> {
|
||||
Node.printWarning("The --clear-network-map-cache flag has been deprecated and will be removed in a future version. Use the clear-network-cache command instead.")
|
||||
networkCacheCli.cmdLineOptions.copyFrom(cmdLineOptions)
|
||||
networkCacheCli.runProgram()
|
||||
}
|
||||
cmdLineOptions.justGenerateNodeInfo -> {
|
||||
Node.printWarning("The --just-generate-node-info flag has been deprecated and will be removed in a future version. Use the generate-node-info command instead.")
|
||||
justGenerateNodeInfoCli.cmdLineOptions.copyFrom(cmdLineOptions)
|
||||
justGenerateNodeInfoCli.runProgram()
|
||||
}
|
||||
cmdLineOptions.justGenerateRpcSslCerts -> {
|
||||
Node.printWarning("The --just-generate-rpc-ssl-settings flag has been deprecated and will be removed in a future version. Use the generate-rpc-ssl-settings command instead.")
|
||||
justGenerateRpcSslCertsCli.cmdLineOptions.copyFrom(cmdLineOptions)
|
||||
justGenerateRpcSslCertsCli.runProgram()
|
||||
}
|
||||
else -> startup.initialiseAndRun(cmdLineOptions, object : RunAfterNodeInitialisation {
|
||||
val startupTime = System.currentTimeMillis()
|
||||
override fun run(node: Node) = startup.startNode(node, startupTime)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Mixin
|
||||
val cmdLineOptions = NodeCmdLineOptions()
|
||||
}
|
||||
|
||||
/** This class provides a common set of functionality for starting a Node from command line arguments. */
|
||||
open class NodeStartup : NodeStartupLogging {
|
||||
companion object {
|
||||
private val logger by lazy { loggerFor<Node>() } // I guess this is lazy to allow for logging init, but why Node?
|
||||
const val LOGS_DIRECTORY_NAME = "logs"
|
||||
const val LOGS_CAN_BE_FOUND_IN_STRING = "Logs can be found in"
|
||||
}
|
||||
|
||||
lateinit var cmdLineOptions: SharedNodeCmdLineOptions
|
||||
|
||||
fun initialiseAndRun(cmdLineOptions: SharedNodeCmdLineOptions, afterNodeInitialisation: RunAfterNodeInitialisation): Int {
|
||||
this.cmdLineOptions = cmdLineOptions
|
||||
|
||||
/**
|
||||
* @return exit code based on the success of the node startup. This value is intended to be the exit code of the process.
|
||||
*/
|
||||
override fun runProgram(): Int {
|
||||
val startTime = System.currentTimeMillis()
|
||||
// Step 1. Check for supported Java version.
|
||||
if (!isValidJavaVersion()) return ExitCodes.FAILURE
|
||||
if (isInvalidJavaVersion()) return ExitCodes.FAILURE
|
||||
|
||||
// Step 2. We do the single node check before we initialise logging so that in case of a double-node start it
|
||||
// doesn't mess with the running node's logs.
|
||||
enforceSingleNodeIsRunning(cmdLineOptions.baseDirectory)
|
||||
|
||||
// Step 3. Initialise logging.
|
||||
initLogging()
|
||||
|
||||
// Step 4. Register all cryptography [Provider]s.
|
||||
// Step 3. Register all cryptography [Provider]s.
|
||||
// Required to install our [SecureRandom] before e.g., UUID asks for one.
|
||||
// This needs to go after initLogging(netty clashes with our logging).
|
||||
// This needs to go after initLogging(netty clashes with our logging)
|
||||
Crypto.registerProviders()
|
||||
|
||||
// Step 5. Print banner and basic node info.
|
||||
// Step 4. Print banner and basic node info.
|
||||
val versionInfo = getVersionInfo()
|
||||
drawBanner(versionInfo)
|
||||
Node.printBasicNodeInfo(LOGS_CAN_BE_FOUND_IN_STRING, System.getProperty("log-path"))
|
||||
|
||||
// Step 6. Load and validate node configuration.
|
||||
val configuration = (attempt { loadConfiguration() }.doOnException(handleConfigurationLoadingError(cmdLineOptions.configFile)) as? Try.Success)?.let(Try.Success<NodeConfiguration>::value) ?: return ExitCodes.FAILURE
|
||||
// Step 5. Load and validate node configuration.
|
||||
val configuration = (attempt { cmdLineOptions.loadConfig() }.doOnException(handleConfigurationLoadingError(cmdLineOptions.configFile)) as? Try.Success)?.let(Try.Success<NodeConfiguration>::value)
|
||||
?: return ExitCodes.FAILURE
|
||||
val errors = configuration.validate()
|
||||
if (errors.isNotEmpty()) {
|
||||
logger.error("Invalid node configuration. Errors were:${System.lineSeparator()}${errors.joinToString(System.lineSeparator())}")
|
||||
return ExitCodes.FAILURE
|
||||
}
|
||||
|
||||
// Step 7. Configuring special serialisation requirements, i.e., bft-smart relies on Java serialization.
|
||||
attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success ?: return ExitCodes.FAILURE
|
||||
// Step 6. Configuring special serialisation requirements, i.e., bft-smart relies on Java serialization.
|
||||
attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success
|
||||
?: return ExitCodes.FAILURE
|
||||
|
||||
// Step 8. Any actions required before starting up the Corda network layer.
|
||||
attempt { preNetworkRegistration(configuration) }.doOnException(handleRegistrationError) as? Try.Success ?: return ExitCodes.FAILURE
|
||||
// Step 7. Any actions required before starting up the Corda network layer.
|
||||
attempt { preNetworkRegistration(configuration) }.doOnException(::handleRegistrationError) as? Try.Success
|
||||
?: return ExitCodes.FAILURE
|
||||
|
||||
// Step 9. Check if in registration mode.
|
||||
checkAndRunRegistrationMode(configuration, versionInfo)?.let {
|
||||
return if (it) ExitCodes.SUCCESS
|
||||
else ExitCodes.FAILURE
|
||||
}
|
||||
|
||||
// Step 10. Log startup info.
|
||||
// Step 8. Log startup info.
|
||||
logStartupInfo(versionInfo, configuration)
|
||||
|
||||
// Step 11. Start node: create the node, check for other command-line options, add extra logging etc.
|
||||
attempt { startNode(configuration, versionInfo, startTime) }.doOnSuccess { logger.info("Node exiting successfully") }.doOnException(handleStartError) as? Try.Success ?: return ExitCodes.FAILURE
|
||||
// Step 9. Start node: create the node, check for other command-line options, add extra logging etc.
|
||||
attempt {
|
||||
cmdLineOptions.baseDirectory.createDirectories()
|
||||
afterNodeInitialisation.run(createNode(configuration, versionInfo))
|
||||
}.doOnException(::handleStartError) as? Try.Success ?: return ExitCodes.FAILURE
|
||||
|
||||
return ExitCodes.SUCCESS
|
||||
}
|
||||
|
||||
private fun checkAndRunRegistrationMode(configuration: NodeConfiguration, versionInfo: VersionInfo): Boolean? {
|
||||
checkUnfinishedRegistration()
|
||||
cmdLineOptions.nodeRegistrationOption?.let {
|
||||
// Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig]
|
||||
attempt { registerWithNetwork(configuration, versionInfo, it) }.doOnException(handleRegistrationError) as? Try.Success
|
||||
?: return false
|
||||
// At this point the node registration was successful. We can delete the marker file.
|
||||
deleteNodeRegistrationMarker(cmdLineOptions.baseDirectory)
|
||||
return true
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO: Reconsider if automatic re-registration should be applied when something failed during initial registration.
|
||||
// There might be cases where the node user should investigate what went wrong before registering again.
|
||||
private fun checkUnfinishedRegistration() {
|
||||
if (checkRegistrationMode() && !cmdLineOptions.isRegistration) {
|
||||
println("Node was started before with `--initial-registration`, but the registration was not completed.\nResuming registration.")
|
||||
// Pretend that the node was started with `--initial-registration` to help prevent user error.
|
||||
cmdLineOptions.isRegistration = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun <RESULT> attempt(action: () -> RESULT): Try<RESULT> = Try.on(action)
|
||||
|
||||
private fun Exception.isExpectedWhenStartingNode() = startNodeExpectedErrors.any { error -> error.isInstance(this) }
|
||||
|
||||
private val startNodeExpectedErrors = setOf(DatabaseMigrationException::class, MultipleCordappsForFlowException::class, CheckpointIncompatibleException::class, AddressBindingException::class, NetworkParametersReader::class, DatabaseIncompatibleException::class)
|
||||
|
||||
private fun Exception.logAsExpected(message: String? = this.message, print: (String?) -> Unit = logger::error) = print(message)
|
||||
|
||||
private fun Exception.logAsUnexpected(message: String? = this.message, error: Exception = this, print: (String?, Throwable) -> Unit = logger::error) = print("$message${this.message?.let { ": $it" } ?: ""}", error)
|
||||
|
||||
private fun Exception.isOpenJdkKnownIssue() = message?.startsWith("Unknown named curve:") == true
|
||||
|
||||
private val handleRegistrationError = { error: Exception ->
|
||||
when (error) {
|
||||
is NodeRegistrationException -> error.logAsExpected("Issue with Node registration: ${error.message}")
|
||||
else -> error.logAsUnexpected("Exception during node registration")
|
||||
}
|
||||
}
|
||||
|
||||
private val handleStartError = { error: Exception ->
|
||||
when {
|
||||
error.isExpectedWhenStartingNode() -> error.logAsExpected()
|
||||
error is CouldNotCreateDataSourceException -> error.logAsUnexpected()
|
||||
error is Errors.NativeIoException && error.message?.contains("Address already in use") == true -> error.logAsExpected("One of the ports required by the Corda node is already in use.")
|
||||
error.isOpenJdkKnownIssue() -> error.logAsExpected("Exception during node startup - ${error.message}. This is a known OpenJDK issue on some Linux distributions, please use OpenJDK from zulu.org or Oracle JDK.")
|
||||
else -> error.logAsUnexpected("Exception during node startup")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleConfigurationLoadingError(configFile: Path) = { error: Exception ->
|
||||
when (error) {
|
||||
is UnknownConfigurationKeysException -> error.logAsExpected()
|
||||
is ConfigException.IO -> error.logAsExpected(configFileNotFoundMessage(configFile), ::println)
|
||||
else -> error.logAsUnexpected("Unexpected error whilst reading node configuration")
|
||||
}
|
||||
}
|
||||
|
||||
private fun configFileNotFoundMessage(configFile: Path): String {
|
||||
return """
|
||||
Unable to load the node config file from '$configFile'.
|
||||
|
||||
Try setting the --base-directory flag to change which directory the node
|
||||
is looking in, or use the --config-file flag to specify it explicitly.
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
private fun loadConfiguration(): NodeConfiguration {
|
||||
val (rawConfig, configurationResult) = loadConfigFile()
|
||||
if (cmdLineOptions.devMode == true) {
|
||||
println("Config:\n${rawConfig.root().render(ConfigRenderOptions.defaults())}")
|
||||
}
|
||||
return configurationResult.getOrThrow()
|
||||
}
|
||||
|
||||
private fun checkRegistrationMode(): Boolean {
|
||||
// If the node was started with `--initial-registration`, create marker file.
|
||||
// We do this here to ensure the marker is created even if parsing the args with NodeArgsParser fails.
|
||||
val marker = cmdLineOptions.baseDirectory / INITIAL_REGISTRATION_MARKER
|
||||
if (!cmdLineOptions.isRegistration && !marker.exists()) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
marker.createFile()
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Could not create marker file for `--initial-registration`.", e)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun deleteNodeRegistrationMarker(baseDir: Path) {
|
||||
try {
|
||||
val marker = File((baseDir / INITIAL_REGISTRATION_MARKER).toUri())
|
||||
if (marker.exists()) {
|
||||
marker.delete()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.logAsUnexpected("Could not delete the marker file that was created for `--initial-registration`.", print = logger::warn)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun preNetworkRegistration(conf: NodeConfiguration) = Unit
|
||||
|
||||
protected open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo)
|
||||
open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo)
|
||||
|
||||
protected open fun startNode(conf: NodeConfiguration, versionInfo: VersionInfo, startTime: Long) {
|
||||
cmdLineOptions.baseDirectory.createDirectories()
|
||||
val node = createNode(conf, versionInfo)
|
||||
if (cmdLineOptions.clearNetworkMapCache) {
|
||||
node.clearNetworkMapCache()
|
||||
return
|
||||
}
|
||||
if (cmdLineOptions.justGenerateNodeInfo) {
|
||||
// Perform the minimum required start-up logic to be able to write a nodeInfo to disk
|
||||
node.generateAndSaveNodeInfo()
|
||||
return
|
||||
}
|
||||
if (cmdLineOptions.justGenerateRpcSslCerts) {
|
||||
generateRpcSslCertificates(conf)
|
||||
return
|
||||
}
|
||||
|
||||
if (conf.devMode) {
|
||||
fun startNode(node: Node, startTime: Long) {
|
||||
if (node.configuration.devMode) {
|
||||
Emoji.renderIfSupported {
|
||||
Node.printWarning("This node is running in developer mode! ${Emoji.developer} This is not safe for production deployment.")
|
||||
Node.printWarning("This node is running in development mode! ${Emoji.developer} This is not safe for production deployment.")
|
||||
}
|
||||
} else {
|
||||
logger.info("The Corda node is running in production mode. If this is a developer environment you can set 'devMode=true' in the node.conf file.")
|
||||
@ -258,7 +194,7 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
|
||||
Node.printBasicNodeInfo("Node for \"$name\" started up and registered in $elapsed sec")
|
||||
|
||||
// Don't start the shell if there's no console attached.
|
||||
if (conf.shouldStartLocalShell()) {
|
||||
if (node.configuration.shouldStartLocalShell()) {
|
||||
node.startupComplete.then {
|
||||
try {
|
||||
InteractiveShell.runLocalShell(node::stop)
|
||||
@ -267,8 +203,8 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (conf.shouldStartSSHDaemon()) {
|
||||
Node.printBasicNodeInfo("SSH server listening on port", conf.sshd!!.port.toString())
|
||||
if (node.configuration.shouldStartSSHDaemon()) {
|
||||
Node.printBasicNodeInfo("SSH server listening on port", node.configuration.sshd!!.port.toString())
|
||||
}
|
||||
},
|
||||
{ th ->
|
||||
@ -277,82 +213,6 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
|
||||
node.run()
|
||||
}
|
||||
|
||||
private fun generateRpcSslCertificates(conf: NodeConfiguration) {
|
||||
val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(conf.myLegalName.x500Principal)
|
||||
|
||||
val keyStorePath = conf.baseDirectory / "certificates" / "rpcsslkeystore.jks"
|
||||
val trustStorePath = conf.baseDirectory / "certificates" / "export" / "rpcssltruststore.jks"
|
||||
|
||||
if (keyStorePath.exists() || trustStorePath.exists()) {
|
||||
println("Found existing RPC SSL keystores. Command was already run. Exiting..")
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
val console: Console? = System.console()
|
||||
|
||||
when (console) {
|
||||
// In this case, the JVM is not connected to the console so we need to exit.
|
||||
null -> {
|
||||
println("Not connected to console. Exiting")
|
||||
exitProcess(1)
|
||||
}
|
||||
// Otherwise we can proceed normally.
|
||||
else -> {
|
||||
while (true) {
|
||||
val keystorePassword1 = console.readPassword("Enter the RPC keystore password => ")
|
||||
// TODO: consider adding a password strength policy.
|
||||
if (keystorePassword1.isEmpty()) {
|
||||
println("The RPC keystore password cannot be an empty String.")
|
||||
continue
|
||||
}
|
||||
|
||||
val keystorePassword2 = console.readPassword("Re-enter the RPC keystore password => ")
|
||||
if (!keystorePassword1.contentEquals(keystorePassword2)) {
|
||||
println("The RPC keystore passwords don't match.")
|
||||
continue
|
||||
}
|
||||
|
||||
saveToKeyStore(keyStorePath, keyPair, cert, String(keystorePassword1), "rpcssl")
|
||||
println("The RPC keystore was saved to: $keyStorePath .")
|
||||
break
|
||||
}
|
||||
|
||||
while (true) {
|
||||
val trustStorePassword1 = console.readPassword("Enter the RPC truststore password => ")
|
||||
// TODO: consider adding a password strength policy.
|
||||
if (trustStorePassword1.isEmpty()) {
|
||||
println("The RPC truststore password cannot be an empty String.")
|
||||
continue
|
||||
}
|
||||
|
||||
val trustStorePassword2 = console.readPassword("Re-enter the RPC truststore password => ")
|
||||
if (!trustStorePassword1.contentEquals(trustStorePassword2)) {
|
||||
println("The RPC truststore passwords don't match.")
|
||||
continue
|
||||
}
|
||||
|
||||
saveToTrustStore(trustStorePath, cert, String(trustStorePassword1), "rpcssl")
|
||||
println("The RPC truststore was saved to: $trustStorePath .")
|
||||
println("You need to distribute this file along with the password in a secure way to all RPC clients.")
|
||||
break
|
||||
}
|
||||
|
||||
val dollar = '$'
|
||||
println("""
|
||||
|
|
||||
|The SSL certificates for RPC were generated successfully.
|
||||
|
|
||||
|Add this snippet to the "rpcSettings" section of your node.conf:
|
||||
| useSsl=true
|
||||
| ssl {
|
||||
| keyStorePath=$dollar{baseDirectory}/certificates/rpcsslkeystore.jks
|
||||
| keyStorePassword=the_above_password
|
||||
| }
|
||||
|""".trimMargin())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun logStartupInfo(versionInfo: VersionInfo, conf: NodeConfiguration) {
|
||||
logger.info("Vendor: ${versionInfo.vendor}")
|
||||
logger.info("Release: ${versionInfo.releaseVersion}")
|
||||
@ -378,34 +238,6 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
|
||||
logger.info(nodeStartedMessage)
|
||||
}
|
||||
|
||||
protected open fun registerWithNetwork(
|
||||
conf: NodeConfiguration,
|
||||
versionInfo: VersionInfo,
|
||||
nodeRegistrationConfig: NodeRegistrationOption
|
||||
) {
|
||||
println("\n" +
|
||||
"******************************************************************\n" +
|
||||
"* *\n" +
|
||||
"* Registering as a new participant with a Corda network *\n" +
|
||||
"* *\n" +
|
||||
"******************************************************************\n")
|
||||
|
||||
NodeRegistrationHelper(conf,
|
||||
HTTPNetworkRegistrationService(
|
||||
requireNotNull(conf.networkServices),
|
||||
versionInfo),
|
||||
nodeRegistrationConfig).buildKeystore()
|
||||
|
||||
// Minimal changes to make registration tool create node identity.
|
||||
// TODO: Move node identity generation logic from node to registration helper.
|
||||
createNode(conf, getVersionInfo()).generateAndSaveNodeInfo()
|
||||
|
||||
println("Successfully registered Corda node with compatibility zone, node identity keys and certificates are stored in '${conf.certificatesDirectory}', it is advised to backup the private keys and certificates.")
|
||||
println("Corda node will now terminate.")
|
||||
}
|
||||
|
||||
protected open fun loadConfigFile(): Pair<Config, Try<NodeConfiguration>> = cmdLineOptions.loadConfig()
|
||||
|
||||
protected open fun banJavaSerialisation(conf: NodeConfiguration) {
|
||||
// Enterprise only - Oracle database requires additional serialization
|
||||
val isOracleDbDriver = conf.dataSourceProperties.getProperty("dataSource.url", "").startsWith("jdbc:oracle:")
|
||||
@ -419,7 +251,7 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
|
||||
SerialFilter.install(serialFilter)
|
||||
}
|
||||
|
||||
protected open fun getVersionInfo(): VersionInfo {
|
||||
open fun getVersionInfo(): VersionInfo {
|
||||
return VersionInfo(
|
||||
PLATFORM_VERSION,
|
||||
CordaVersionProvider.releaseVersion,
|
||||
@ -472,18 +304,6 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
|
||||
}
|
||||
}
|
||||
|
||||
override fun initLogging() {
|
||||
val loggingLevel = loggingLevel.name.toLowerCase(Locale.ENGLISH)
|
||||
System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file.
|
||||
if (verbose) {
|
||||
System.setProperty("consoleLogLevel", loggingLevel)
|
||||
Node.renderBasicInfoToConsole = false
|
||||
}
|
||||
System.setProperty("log-path", (cmdLineOptions.baseDirectory / LOGS_DIRECTORY_NAME).toString())
|
||||
SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler.
|
||||
SLF4JBridgeHandler.install()
|
||||
}
|
||||
|
||||
private fun lookupMachineNameAndMaybeWarn(): String {
|
||||
val start = System.currentTimeMillis()
|
||||
val hostName: String = InetAddress.getLocalHost().hostName
|
||||
@ -587,3 +407,66 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
|
||||
}
|
||||
}
|
||||
|
||||
/** Provide some common logging methods for node startup commands. */
|
||||
interface NodeStartupLogging {
|
||||
companion object {
|
||||
val logger by lazy { contextLogger() }
|
||||
val startupErrors = setOf(MultipleCordappsForFlowException::class, CheckpointIncompatibleException::class, AddressBindingException::class, NetworkParametersReader::class, DatabaseIncompatibleException::class)
|
||||
}
|
||||
|
||||
fun <RESULT> attempt(action: () -> RESULT): Try<RESULT> = Try.on(action)
|
||||
|
||||
fun Exception.logAsExpected(message: String? = this.message, print: (String?) -> Unit = logger::error) = print(message)
|
||||
|
||||
fun Exception.logAsUnexpected(message: String? = this.message, error: Exception = this, print: (String?, Throwable) -> Unit = logger::error) = print("$message${this.message?.let { ": $it" } ?: ""}", error)
|
||||
|
||||
fun handleRegistrationError(error: Exception) {
|
||||
when (error) {
|
||||
is NodeRegistrationException -> error.logAsExpected("Issue with Node registration: ${error.message}")
|
||||
else -> error.logAsUnexpected("Exception during node registration")
|
||||
}
|
||||
}
|
||||
|
||||
fun Exception.isOpenJdkKnownIssue() = message?.startsWith("Unknown named curve:") == true
|
||||
|
||||
fun Exception.isExpectedWhenStartingNode() = startupErrors.any { error -> error.isInstance(this) }
|
||||
|
||||
fun handleStartError(error: Exception) {
|
||||
when {
|
||||
error.isExpectedWhenStartingNode() -> error.logAsExpected()
|
||||
error is CouldNotCreateDataSourceException -> error.logAsUnexpected()
|
||||
error is Errors.NativeIoException && error.message?.contains("Address already in use") == true -> error.logAsExpected("One of the ports required by the Corda node is already in use.")
|
||||
error.isOpenJdkKnownIssue() -> error.logAsExpected("Exception during node startup - ${error.message}. This is a known OpenJDK issue on some Linux distributions, please use OpenJDK from zulu.org or Oracle JDK.")
|
||||
else -> error.logAsUnexpected("Exception during node startup")
|
||||
}
|
||||
}
|
||||
|
||||
fun handleConfigurationLoadingError(configFile: Path) = { error: Exception ->
|
||||
when (error) {
|
||||
is UnknownConfigurationKeysException -> error.logAsExpected()
|
||||
is ConfigException.IO -> error.logAsExpected(configFileNotFoundMessage(configFile), ::println)
|
||||
else -> error.logAsUnexpected("Unexpected error whilst reading node configuration")
|
||||
}
|
||||
}
|
||||
|
||||
private fun configFileNotFoundMessage(configFile: Path): String {
|
||||
return """
|
||||
Unable to load the node config file from '$configFile'.
|
||||
|
||||
Try setting the --base-directory flag to change which directory the node
|
||||
is looking in, or use the --config-file flag to specify it explicitly.
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
fun CliWrapperBase.initLogging(baseDirectory: Path) {
|
||||
val loggingLevel = loggingLevel.name.toLowerCase(Locale.ENGLISH)
|
||||
System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file.
|
||||
if (verbose) {
|
||||
System.setProperty("consoleLogLevel", loggingLevel)
|
||||
Node.renderBasicInfoToConsole = false
|
||||
}
|
||||
System.setProperty("log-path", (baseDirectory / NodeCliCommand.LOGS_DIRECTORY_NAME).toString())
|
||||
SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler.
|
||||
SLF4JBridgeHandler.install()
|
||||
}
|
||||
|
@ -5,30 +5,26 @@ import com.typesafe.config.ConfigFactory
|
||||
import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.core.internal.isDirectory
|
||||
import net.corda.core.internal.noneOrSingle
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
class CordappConfigFileProvider(private val configDir: Path = DEFAULT_CORDAPP_CONFIG_DIR) : CordappConfigProvider {
|
||||
class CordappConfigFileProvider(cordappDirectories: List<Path>) : CordappConfigProvider {
|
||||
companion object {
|
||||
val DEFAULT_CORDAPP_CONFIG_DIR = Paths.get("cordapps") / "config"
|
||||
const val CONFIG_EXT = ".conf"
|
||||
val logger = contextLogger()
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
init {
|
||||
configDir.createDirectories()
|
||||
}
|
||||
private val configDirectories = cordappDirectories.map { (it / "config").createDirectories() }
|
||||
|
||||
override fun getConfigByName(name: String): Config {
|
||||
val configFile = configDir / "$name$CONFIG_EXT"
|
||||
return if (configFile.exists()) {
|
||||
check(!configFile.isDirectory()) { "${configFile.toAbsolutePath()} is a directory, expected a config file" }
|
||||
logger.info("Found config for cordapp $name in ${configFile.toAbsolutePath()}")
|
||||
// TODO There's nothing stopping the same CorDapp jar from occuring in different directories and thus causing
|
||||
// conflicts. The cordappDirectories list config option should just be a single cordappDirectory
|
||||
val configFile = configDirectories.map { it / "$name.conf" }.noneOrSingle { it.exists() }
|
||||
return if (configFile != null) {
|
||||
logger.info("Found config for cordapp $name in $configFile")
|
||||
ConfigFactory.parseFile(configFile.toFile())
|
||||
} else {
|
||||
logger.info("No config found for cordapp $name in ${configFile.toAbsolutePath()}")
|
||||
logger.info("No config found for cordapp $name in $configDirectories")
|
||||
ConfigFactory.empty()
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ import net.corda.core.serialization.SerializeAsToken
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.node.cordapp.CordappLoader
|
||||
import net.corda.node.internal.classloading.requireAnnotation
|
||||
import net.corda.nodeapi.internal.coreContractClasses
|
||||
import net.corda.serialization.internal.DefaultWhitelist
|
||||
import org.apache.commons.collections4.map.LRUMap
|
||||
@ -29,6 +28,7 @@ import java.lang.reflect.Modifier
|
||||
import java.net.URL
|
||||
import java.net.URLClassLoader
|
||||
import java.nio.file.Path
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import java.util.jar.JarInputStream
|
||||
import kotlin.reflect.KClass
|
||||
@ -41,7 +41,8 @@ import kotlin.streams.toList
|
||||
*/
|
||||
class JarScanningCordappLoader private constructor(private val cordappJarPaths: List<RestrictedURL>,
|
||||
private val versionInfo: VersionInfo = VersionInfo.UNKNOWN,
|
||||
extraCordapps: List<CordappImpl>) : CordappLoaderTemplate() {
|
||||
extraCordapps: List<CordappImpl>,
|
||||
private val blacklistedCordappSigners: List<X509Certificate> = emptyList()) : CordappLoaderTemplate() {
|
||||
|
||||
override val cordapps: List<CordappImpl> by lazy {
|
||||
loadCordapps() + extraCordapps
|
||||
@ -67,10 +68,11 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
*/
|
||||
fun fromDirectories(cordappDirs: Collection<Path>,
|
||||
versionInfo: VersionInfo = VersionInfo.UNKNOWN,
|
||||
extraCordapps: List<CordappImpl> = emptyList()): JarScanningCordappLoader {
|
||||
extraCordapps: List<CordappImpl> = emptyList(),
|
||||
blacklistedCerts: List<X509Certificate> = emptyList()): JarScanningCordappLoader {
|
||||
logger.info("Looking for CorDapps in ${cordappDirs.distinct().joinToString(", ", "[", "]")}")
|
||||
val paths = cordappDirs.distinct().flatMap(this::jarUrlsInDirectory).map { it.restricted() }
|
||||
return JarScanningCordappLoader(paths, versionInfo, extraCordapps)
|
||||
return JarScanningCordappLoader(paths, versionInfo, extraCordapps, blacklistedCerts)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -78,9 +80,9 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
*
|
||||
* @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection.
|
||||
*/
|
||||
fun fromJarUrls(scanJars: List<URL>, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List<CordappImpl> = emptyList()): JarScanningCordappLoader {
|
||||
fun fromJarUrls(scanJars: List<URL>, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List<CordappImpl> = emptyList(), blacklistedCerts: List<X509Certificate> = emptyList()): JarScanningCordappLoader {
|
||||
val paths = scanJars.map { it.restricted() }
|
||||
return JarScanningCordappLoader(paths, versionInfo, extraCordapps)
|
||||
return JarScanningCordappLoader(paths, versionInfo, extraCordapps, blacklistedCerts)
|
||||
}
|
||||
|
||||
private fun URL.restricted(rootPackageName: String? = null) = RestrictedURL(this, rootPackageName)
|
||||
@ -109,6 +111,20 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
true
|
||||
}
|
||||
}
|
||||
.filter {
|
||||
if (blacklistedCordappSigners.isEmpty()) {
|
||||
true //Nothing blacklisted, no need to check
|
||||
} else {
|
||||
val certificates = it.jarPath.openStream().let(::JarInputStream).use(JarSignatureCollector::collectCertificates)
|
||||
if (certificates.isEmpty() || (certificates - blacklistedCordappSigners).isNotEmpty())
|
||||
true // Cordapp is not signed or it is signed by at least one non-blacklisted certificate
|
||||
else {
|
||||
logger.warn("Not loading CorDapp ${it.info.shortName} (${it.info.vendor}) as it is signed by development key(s) only: " +
|
||||
"${certificates.intersect(blacklistedCordappSigners).map { it.publicKey }}.")
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
cordapps.forEach { CordappInfoResolver.register(it.cordappClasses, it.info) }
|
||||
return cordapps
|
||||
}
|
||||
@ -151,17 +167,6 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
|
||||
private fun findInitiatedFlows(scanResult: RestrictedScanResult): List<Class<out FlowLogic<*>>> {
|
||||
return scanResult.getClassesWithAnnotation(FlowLogic::class, InitiatedBy::class)
|
||||
// First group by the initiating flow class in case there are multiple mappings
|
||||
.groupBy { it.requireAnnotation<InitiatedBy>().value.java }
|
||||
.map { (initiatingFlow, initiatedFlows) ->
|
||||
val sorted = initiatedFlows.sortedWith(FlowTypeHierarchyComparator(initiatingFlow))
|
||||
if (sorted.size > 1) {
|
||||
logger.warn("${initiatingFlow.name} has been specified as the inititating flow by multiple flows " +
|
||||
"in the same type hierarchy: ${sorted.joinToString { it.name }}. Choosing the most " +
|
||||
"specific sub-type for registration: ${sorted[0].name}.")
|
||||
}
|
||||
sorted[0]
|
||||
}
|
||||
}
|
||||
|
||||
private fun Class<out FlowLogic<*>>.isUserInvokable(): Boolean {
|
||||
@ -216,17 +221,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
}
|
||||
}
|
||||
|
||||
private class FlowTypeHierarchyComparator(val initiatingFlow: Class<out FlowLogic<*>>) : Comparator<Class<out FlowLogic<*>>> {
|
||||
override fun compare(o1: Class<out FlowLogic<*>>, o2: Class<out FlowLogic<*>>): Int {
|
||||
return when {
|
||||
o1 == o2 -> 0
|
||||
o1.isAssignableFrom(o2) -> 1
|
||||
o2.isAssignableFrom(o1) -> -1
|
||||
else -> throw IllegalArgumentException("${initiatingFlow.name} has been specified as the initiating flow by " +
|
||||
"both ${o1.name} and ${o2.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun <T : Any> loadClass(className: String, type: KClass<T>): Class<out T>? {
|
||||
return try {
|
||||
|
@ -0,0 +1,14 @@
|
||||
package net.corda.node.internal.subcommands
|
||||
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.internal.NodeCliCommand
|
||||
import net.corda.node.internal.NodeStartup
|
||||
import net.corda.node.internal.RunAfterNodeInitialisation
|
||||
|
||||
class ClearNetworkCacheCli(startup: NodeStartup): NodeCliCommand("clear-network-cache", "Clears local copy of network map, on node startup it will be restored from server or file system.", startup) {
|
||||
override fun runProgram(): Int {
|
||||
return startup.initialiseAndRun(cmdLineOptions, object: RunAfterNodeInitialisation {
|
||||
override fun run(node: Node) = node.clearNetworkMapCache()
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package net.corda.node.internal.subcommands
|
||||
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.internal.NodeCliCommand
|
||||
import net.corda.node.internal.NodeStartup
|
||||
import net.corda.node.internal.RunAfterNodeInitialisation
|
||||
|
||||
class GenerateNodeInfoCli(startup: NodeStartup): NodeCliCommand("generate-node-info", "Performs the node start-up tasks necessary to generate the nodeInfo file, saves it to disk, then exits.", startup) {
|
||||
override fun runProgram(): Int {
|
||||
return startup.initialiseAndRun(cmdLineOptions, object : RunAfterNodeInitialisation {
|
||||
override fun run(node: Node) {
|
||||
node.generateAndSaveNodeInfo()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
package net.corda.node.internal.subcommands
|
||||
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.internal.NodeCliCommand
|
||||
import net.corda.node.internal.NodeStartup
|
||||
import net.corda.node.internal.RunAfterNodeInitialisation
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate
|
||||
import net.corda.node.utilities.saveToKeyStore
|
||||
import net.corda.node.utilities.saveToTrustStore
|
||||
import java.io.Console
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class GenerateRpcSslCertsCli(startup: NodeStartup): NodeCliCommand("generate-rpc-ssl-settings", "Generates the SSL key and trust stores for a secure RPC connection.", startup) {
|
||||
override fun runProgram(): Int {
|
||||
return startup.initialiseAndRun(cmdLineOptions, GenerateRpcSslCerts())
|
||||
}
|
||||
}
|
||||
|
||||
class GenerateRpcSslCerts: RunAfterNodeInitialisation {
|
||||
override fun run(node: Node) {
|
||||
generateRpcSslCertificates(node.configuration)
|
||||
}
|
||||
|
||||
private fun generateRpcSslCertificates(conf: NodeConfiguration) {
|
||||
val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(conf.myLegalName.x500Principal)
|
||||
|
||||
val keyStorePath = conf.baseDirectory / "certificates" / "rpcsslkeystore.jks"
|
||||
val trustStorePath = conf.baseDirectory / "certificates" / "export" / "rpcssltruststore.jks"
|
||||
|
||||
if (keyStorePath.exists() || trustStorePath.exists()) {
|
||||
println("Found existing RPC SSL keystores. Command was already run. Exiting.")
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
val console: Console? = System.console()
|
||||
|
||||
when (console) {
|
||||
// In this case, the JVM is not connected to the console so we need to exit.
|
||||
null -> {
|
||||
println("Not connected to console. Exiting.")
|
||||
exitProcess(1)
|
||||
}
|
||||
// Otherwise we can proceed normally.
|
||||
else -> {
|
||||
while (true) {
|
||||
val keystorePassword1 = console.readPassword("Enter the RPC keystore password:")
|
||||
// TODO: consider adding a password strength policy.
|
||||
if (keystorePassword1.isEmpty()) {
|
||||
println("The RPC keystore password cannot be an empty String.")
|
||||
continue
|
||||
}
|
||||
|
||||
val keystorePassword2 = console.readPassword("Re-enter the RPC keystore password:")
|
||||
if (!keystorePassword1.contentEquals(keystorePassword2)) {
|
||||
println("The RPC keystore passwords don't match.")
|
||||
continue
|
||||
}
|
||||
|
||||
saveToKeyStore(keyStorePath, keyPair, cert, String(keystorePassword1), "rpcssl")
|
||||
println("The RPC keystore was saved to: $keyStorePath .")
|
||||
break
|
||||
}
|
||||
|
||||
while (true) {
|
||||
val trustStorePassword1 = console.readPassword("Enter the RPC truststore password:")
|
||||
// TODO: consider adding a password strength policy.
|
||||
if (trustStorePassword1.isEmpty()) {
|
||||
println("The RPC truststore password cannot be an empty string.")
|
||||
continue
|
||||
}
|
||||
|
||||
val trustStorePassword2 = console.readPassword("Re-enter the RPC truststore password:")
|
||||
if (!trustStorePassword1.contentEquals(trustStorePassword2)) {
|
||||
println("The RPC truststore passwords don't match.")
|
||||
continue
|
||||
}
|
||||
|
||||
saveToTrustStore(trustStorePath, cert, String(trustStorePassword1), "rpcssl")
|
||||
println("The RPC truststore was saved to: $trustStorePath.")
|
||||
println("You need to distribute this file along with the password in a secure way to all RPC clients.")
|
||||
break
|
||||
}
|
||||
|
||||
val dollar = '$'
|
||||
println("""
|
||||
|
|
||||
|The SSL certificates for RPC were generated successfully.
|
||||
|
|
||||
|Add this snippet to the "rpcSettings" section of your node.conf:
|
||||
| useSsl=true
|
||||
| ssl {
|
||||
| keyStorePath=$dollar{baseDirectory}/certificates/rpcsslkeystore.jks
|
||||
| keyStorePassword=the_above_password
|
||||
| }
|
||||
|""".trimMargin())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
package net.corda.node.internal.subcommands
|
||||
|
||||
import net.corda.cliutils.CliWrapperBase
|
||||
import net.corda.core.internal.createFile
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.node.InitialRegistrationCmdLineOptions
|
||||
import net.corda.node.NodeRegistrationOption
|
||||
import net.corda.node.internal.*
|
||||
import net.corda.node.internal.NodeStartupLogging.Companion.logger
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
|
||||
import net.corda.node.utilities.registration.NodeRegistrationHelper
|
||||
import picocli.CommandLine.Mixin
|
||||
import picocli.CommandLine.Option
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
|
||||
class InitialRegistrationCli(val startup: NodeStartup): CliWrapperBase("initial-registration", "Starts initial node registration with Corda network to obtain certificate from the permissioning server.") {
|
||||
@Option(names = ["-t", "--network-root-truststore"], description = ["Network root trust store obtained from network operator."])
|
||||
var networkRootTrustStorePathParameter: Path? = null
|
||||
|
||||
@Option(names = ["-p", "--network-root-truststore-password"], description = ["Network root trust store password obtained from network operator."], required = true)
|
||||
var networkRootTrustStorePassword: String = ""
|
||||
|
||||
override fun runProgram() : Int {
|
||||
val networkRootTrustStorePath: Path = networkRootTrustStorePathParameter ?: cmdLineOptions.baseDirectory / "certificates" / "network-root-truststore.jks"
|
||||
return startup.initialiseAndRun(cmdLineOptions, InitialRegistration(cmdLineOptions.baseDirectory, networkRootTrustStorePath, networkRootTrustStorePassword, startup))
|
||||
}
|
||||
|
||||
override fun initLogging() = this.initLogging(cmdLineOptions.baseDirectory)
|
||||
|
||||
@Mixin
|
||||
val cmdLineOptions = InitialRegistrationCmdLineOptions()
|
||||
}
|
||||
|
||||
class InitialRegistration(val baseDirectory: Path, private val networkRootTrustStorePath: Path, networkRootTrustStorePassword: String, private val startup: NodeStartup) : RunAfterNodeInitialisation, NodeStartupLogging {
|
||||
companion object {
|
||||
private const val INITIAL_REGISTRATION_MARKER = ".initialregistration"
|
||||
|
||||
fun checkRegistrationMode(baseDirectory: Path): Boolean {
|
||||
// If the node was started with `--initial-registration`, create marker file.
|
||||
// We do this here to ensure the marker is created even if parsing the args with NodeArgsParser fails.
|
||||
val marker = baseDirectory / INITIAL_REGISTRATION_MARKER
|
||||
if (!marker.exists()) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
marker.createFile()
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Could not create marker file for `initial-registration`.", e)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private val nodeRegistration = NodeRegistrationOption(networkRootTrustStorePath, networkRootTrustStorePassword)
|
||||
|
||||
private fun registerWithNetwork(conf: NodeConfiguration) {
|
||||
val versionInfo = startup.getVersionInfo()
|
||||
|
||||
println("\n" +
|
||||
"******************************************************************\n" +
|
||||
"* *\n" +
|
||||
"* Registering as a new participant with a Corda network *\n" +
|
||||
"* *\n" +
|
||||
"******************************************************************\n")
|
||||
|
||||
NodeRegistrationHelper(conf,
|
||||
HTTPNetworkRegistrationService(
|
||||
requireNotNull(conf.networkServices),
|
||||
versionInfo),
|
||||
nodeRegistration).buildKeystore()
|
||||
|
||||
// Minimal changes to make registration tool create node identity.
|
||||
// TODO: Move node identity generation logic from node to registration helper.
|
||||
startup.createNode(conf, versionInfo).generateAndSaveNodeInfo()
|
||||
|
||||
println("Successfully registered Corda node with compatibility zone, node identity keys and certificates are stored in '${conf.certificatesDirectory}', it is advised to backup the private keys and certificates.")
|
||||
println("Corda node will now terminate.")
|
||||
}
|
||||
|
||||
private fun initialRegistration(config: NodeConfiguration) {
|
||||
// Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig]
|
||||
attempt { registerWithNetwork(config) }.doOnException(this::handleRegistrationError) as? Try.Success
|
||||
// At this point the node registration was successful. We can delete the marker file.
|
||||
deleteNodeRegistrationMarker(baseDirectory)
|
||||
}
|
||||
|
||||
private fun deleteNodeRegistrationMarker(baseDir: Path) {
|
||||
try {
|
||||
val marker = File((baseDir / INITIAL_REGISTRATION_MARKER).toUri())
|
||||
if (marker.exists()) {
|
||||
marker.delete()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.logAsUnexpected( "Could not delete the marker file that was created for `initial-registration`.", print = logger::warn)
|
||||
}
|
||||
}
|
||||
|
||||
override fun run(node: Node) {
|
||||
require(networkRootTrustStorePath.exists()) { "Network root trust store path: '$networkRootTrustStorePath' doesn't exist" }
|
||||
if (checkRegistrationMode(baseDirectory)) {
|
||||
println("Node was started before with `--initial-registration`, but the registration was not completed.\nResuming registration.")
|
||||
}
|
||||
initialRegistration(node.configuration)
|
||||
}
|
||||
}
|
||||
|
@ -82,6 +82,7 @@ interface NodeConfiguration {
|
||||
val p2pSslOptions: MutualSslConfiguration
|
||||
|
||||
val cordappDirectories: List<Path>
|
||||
val flowOverrides: FlowOverrideConfig?
|
||||
|
||||
fun validate(): List<String>
|
||||
|
||||
@ -103,6 +104,9 @@ interface NodeConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
data class FlowOverrideConfig(val overrides: List<FlowOverride> = listOf())
|
||||
data class FlowOverride(val initiator: String, val responder: String)
|
||||
|
||||
/**
|
||||
* Currently registered JMX Reporters
|
||||
*/
|
||||
@ -229,7 +233,8 @@ data class NodeConfigurationImpl(
|
||||
override val flowMonitorSuspensionLoggingThresholdMillis: Duration = DEFAULT_FLOW_MONITOR_SUSPENSION_LOGGING_THRESHOLD_MILLIS,
|
||||
override val cordappDirectories: List<Path> = listOf(baseDirectory / CORDAPPS_DIR_NAME_DEFAULT),
|
||||
override val jmxReporterType: JmxReporterType? = JmxReporterType.JOLOKIA,
|
||||
private val useOpenSsl: Boolean = false
|
||||
private val useOpenSsl: Boolean = false,
|
||||
override val flowOverrides: FlowOverrideConfig?
|
||||
) : NodeConfiguration {
|
||||
companion object {
|
||||
private val logger = loggerFor<NodeConfigurationImpl>()
|
||||
|
@ -26,11 +26,6 @@
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "--install-shell-extensions"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "--just-generate-node-info"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
@ -99,11 +94,6 @@
|
||||
required: false
|
||||
multiParam: true
|
||||
acceptableValues: []
|
||||
- parameterName: "-c"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "-d"
|
||||
parameterType: "java.lang.Boolean"
|
||||
required: false
|
@ -12,7 +12,6 @@ import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockNodeParameters
|
||||
import net.corda.testing.node.StartedMockNode
|
||||
import org.assertj.core.api.Assertions.assertThatIllegalStateException
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@ -39,15 +38,15 @@ class FlowRegistrationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startup fails when two flows initiated by the same flow are registered`() {
|
||||
fun `succeeds when a subclass of a flow initiated by the same flow is registered`() {
|
||||
// register the same flow twice to invoke the error without causing errors in other tests
|
||||
responder.registerInitiatedFlow(Responder::class.java)
|
||||
assertThatIllegalStateException().isThrownBy { responder.registerInitiatedFlow(Responder::class.java) }
|
||||
responder.registerInitiatedFlow(Responder1::class.java)
|
||||
responder.registerInitiatedFlow(Responder1Subclassed::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a single initiated flow can be registered without error`() {
|
||||
responder.registerInitiatedFlow(Responder::class.java)
|
||||
responder.registerInitiatedFlow(Responder1::class.java)
|
||||
val result = initiator.startFlow(Initiator(responder.info.singleIdentity()))
|
||||
mockNetwork.runNetwork()
|
||||
assertNotNull(result.get())
|
||||
@ -63,7 +62,38 @@ class Initiator(val party: Party) : FlowLogic<String>() {
|
||||
}
|
||||
|
||||
@InitiatedBy(Initiator::class)
|
||||
private class Responder(val session: FlowSession) : FlowLogic<Unit>() {
|
||||
private open class Responder1(val session: FlowSession) : FlowLogic<Unit>() {
|
||||
open fun getPayload(): String {
|
||||
return "whats up"
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
session.receive<String>().unwrap { it }
|
||||
session.send("What's up")
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(Initiator::class)
|
||||
private open class Responder2(val session: FlowSession) : FlowLogic<Unit>() {
|
||||
open fun getPayload(): String {
|
||||
return "whats up"
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
session.receive<String>().unwrap { it }
|
||||
session.send("What's up")
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(Initiator::class)
|
||||
private class Responder1Subclassed(session: FlowSession) : Responder1(session) {
|
||||
|
||||
override fun getPayload(): String {
|
||||
return "im subclassed! that's what's up!"
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
session.receive<String>().unwrap { it }
|
||||
|
@ -0,0 +1,110 @@
|
||||
package net.corda.node.internal
|
||||
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.node.services.config.FlowOverride
|
||||
import net.corda.node.services.config.FlowOverrideConfig
|
||||
import org.hamcrest.CoreMatchers.`is`
|
||||
import org.hamcrest.CoreMatchers.instanceOf
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.mockito.Mockito
|
||||
import java.lang.IllegalStateException
|
||||
|
||||
private val marker = "This is a special marker"
|
||||
|
||||
class NodeFlowManagerTest {
|
||||
|
||||
@InitiatingFlow
|
||||
class Init : FlowLogic<Unit>() {
|
||||
override fun call() {
|
||||
TODO("not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(Init::class)
|
||||
open class Resp(val otherSesh: FlowSession) : FlowLogic<Unit>() {
|
||||
override fun call() {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@InitiatedBy(Init::class)
|
||||
class Resp2(val otherSesh: FlowSession) : FlowLogic<Unit>() {
|
||||
override fun call() {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@InitiatedBy(Init::class)
|
||||
open class RespSub(sesh: FlowSession) : Resp(sesh) {
|
||||
override fun call() {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@InitiatedBy(Init::class)
|
||||
class RespSubSub(sesh: FlowSession) : RespSub(sesh) {
|
||||
override fun call() {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun `should fail to validate if more than one registration with equal weight`() {
|
||||
val nodeFlowManager = NodeFlowManager()
|
||||
nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp::class.java)
|
||||
nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp2::class.java)
|
||||
nodeFlowManager.validateRegistrations()
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun `should allow registration of flows with different weights`() {
|
||||
val nodeFlowManager = NodeFlowManager()
|
||||
nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp::class.java)
|
||||
nodeFlowManager.registerInitiatedFlow(Init::class.java, RespSub::class.java)
|
||||
nodeFlowManager.validateRegistrations()
|
||||
val factory = nodeFlowManager.getFlowFactoryForInitiatingFlow(Init::class.java)!!
|
||||
val flow = factory.createFlow(Mockito.mock(FlowSession::class.java))
|
||||
Assert.assertThat(flow, `is`(instanceOf(RespSub::class.java)))
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun `should allow updating of registered responder at runtime`() {
|
||||
val nodeFlowManager = NodeFlowManager()
|
||||
nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp::class.java)
|
||||
nodeFlowManager.registerInitiatedFlow(Init::class.java, RespSub::class.java)
|
||||
nodeFlowManager.validateRegistrations()
|
||||
var factory = nodeFlowManager.getFlowFactoryForInitiatingFlow(Init::class.java)!!
|
||||
var flow = factory.createFlow(Mockito.mock(FlowSession::class.java))
|
||||
Assert.assertThat(flow, `is`(instanceOf(RespSub::class.java)))
|
||||
// update
|
||||
nodeFlowManager.registerInitiatedFlow(Init::class.java, RespSubSub::class.java)
|
||||
nodeFlowManager.validateRegistrations()
|
||||
|
||||
factory = nodeFlowManager.getFlowFactoryForInitiatingFlow(Init::class.java)!!
|
||||
flow = factory.createFlow(Mockito.mock(FlowSession::class.java))
|
||||
Assert.assertThat(flow, `is`(instanceOf(RespSubSub::class.java)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should allow an override to be specified`() {
|
||||
val nodeFlowManager = NodeFlowManager(FlowOverrideConfig(listOf(FlowOverride(Init::class.qualifiedName!!, Resp::class.qualifiedName!!))))
|
||||
nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp::class.java)
|
||||
nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp2::class.java)
|
||||
nodeFlowManager.registerInitiatedFlow(Init::class.java, RespSubSub::class.java)
|
||||
nodeFlowManager.validateRegistrations()
|
||||
|
||||
val factory = nodeFlowManager.getFlowFactoryForInitiatingFlow(Init::class.java)!!
|
||||
val flow = factory.createFlow(Mockito.mock(FlowSession::class.java))
|
||||
|
||||
Assert.assertThat(flow, `is`(instanceOf(Resp::class.java)))
|
||||
}
|
||||
}
|
@ -2,4 +2,4 @@ package net.corda.node.internal
|
||||
|
||||
import net.corda.testing.CliBackwardsCompatibleTest
|
||||
|
||||
class NodeStartupCompatibilityTest : CliBackwardsCompatibleTest(NodeStartup::class.java)
|
||||
class NodeStartupCompatibilityTest : CliBackwardsCompatibleTest(NodeStartupCli::class.java)
|
@ -1,6 +1,8 @@
|
||||
package net.corda.node.internal
|
||||
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.node.InitialRegistrationCmdLineOptions
|
||||
import net.corda.node.internal.subcommands.InitialRegistrationCli
|
||||
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.BeforeClass
|
||||
@ -11,7 +13,7 @@ import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
class NodeStartupTest {
|
||||
private val startup = NodeStartup()
|
||||
private val startup = NodeStartupCli()
|
||||
|
||||
companion object {
|
||||
private lateinit var workingDirectory: Path
|
||||
@ -30,7 +32,6 @@ class NodeStartupTest {
|
||||
assertThat(startup.cmdLineOptions.configFile).isEqualTo(workingDirectory / "node.conf")
|
||||
assertThat(startup.verbose).isEqualTo(false)
|
||||
assertThat(startup.loggingLevel).isEqualTo(Level.INFO)
|
||||
assertThat(startup.cmdLineOptions.nodeRegistrationOption).isEqualTo(null)
|
||||
assertThat(startup.cmdLineOptions.noLocalShell).isEqualTo(false)
|
||||
assertThat(startup.cmdLineOptions.sshdServer).isEqualTo(false)
|
||||
assertThat(startup.cmdLineOptions.justGenerateNodeInfo).isEqualTo(false)
|
||||
@ -38,7 +39,7 @@ class NodeStartupTest {
|
||||
assertThat(startup.cmdLineOptions.unknownConfigKeysPolicy).isEqualTo(UnknownConfigKeysPolicy.FAIL)
|
||||
assertThat(startup.cmdLineOptions.devMode).isEqualTo(null)
|
||||
assertThat(startup.cmdLineOptions.clearNetworkMapCache).isEqualTo(false)
|
||||
assertThat(startup.cmdLineOptions.networkRootTrustStorePath).isEqualTo(workingDirectory / "certificates" / "network-root-truststore.jks")
|
||||
assertThat(startup.cmdLineOptions.networkRootTrustStorePathParameter).isEqualTo(null)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -46,6 +47,6 @@ class NodeStartupTest {
|
||||
CommandLine.populateCommand(startup, "--base-directory", (workingDirectory / "another-base-dir").toString())
|
||||
assertThat(startup.cmdLineOptions.baseDirectory).isEqualTo(workingDirectory / "another-base-dir")
|
||||
assertThat(startup.cmdLineOptions.configFile).isEqualTo(workingDirectory / "another-base-dir" / "node.conf")
|
||||
assertThat(startup.cmdLineOptions.networkRootTrustStorePath).isEqualTo(workingDirectory / "another-base-dir" / "certificates" / "network-root-truststore.jks")
|
||||
assertThat(startup.cmdLineOptions.networkRootTrustStorePathParameter).isEqualTo(null)
|
||||
}
|
||||
}
|
||||
|
@ -149,7 +149,7 @@ class NodeTest {
|
||||
}
|
||||
}
|
||||
|
||||
private fun createConfig(nodeName: CordaX500Name): NodeConfiguration {
|
||||
private fun createConfig(nodeName: CordaX500Name): NodeConfigurationImpl {
|
||||
val fakeAddress = NetworkHostAndPort("0.1.2.3", 456)
|
||||
return NodeConfigurationImpl(
|
||||
baseDirectory = temporaryFolder.root.toPath(),
|
||||
@ -171,7 +171,8 @@ class NodeTest {
|
||||
enterpriseConfiguration = EnterpriseConfiguration(
|
||||
mutualExclusionConfiguration = MutualExclusionConfiguration(updateInterval = 0, waitInterval = 0)
|
||||
),
|
||||
relay = null
|
||||
relay = null,
|
||||
flowOverrides = FlowOverrideConfig(listOf())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -12,16 +12,16 @@ import java.nio.file.Paths
|
||||
|
||||
class CordappConfigFileProviderTests {
|
||||
private companion object {
|
||||
val cordappConfDir = Paths.get("build") / "tmp" / "cordapps" / "config"
|
||||
val cordappDir = Paths.get("build") / "tmp" / "cordapps"
|
||||
const val cordappName = "test"
|
||||
val cordappConfFile = cordappConfDir / "$cordappName.conf"
|
||||
val cordappConfFile = cordappDir / "config" / "$cordappName.conf"
|
||||
|
||||
val validConfig: Config = ConfigFactory.parseString("key=value")
|
||||
val alternateValidConfig: Config = ConfigFactory.parseString("key=alternateValue")
|
||||
const val invalidConfig = "Invalid"
|
||||
}
|
||||
|
||||
private val provider = CordappConfigFileProvider(cordappConfDir)
|
||||
private val provider = CordappConfigFileProvider(listOf(cordappDir))
|
||||
|
||||
@Test
|
||||
fun `test that config can be loaded`() {
|
||||
|
@ -6,6 +6,7 @@ import net.corda.core.internal.packageName
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.testing.node.internal.TestCordappDirectories
|
||||
import net.corda.testing.node.internal.cordappForPackages
|
||||
import net.corda.nodeapi.internal.DEV_CERTIFICATES
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
import java.nio.file.Paths
|
||||
@ -56,7 +57,7 @@ class JarScanningCordappLoaderTest {
|
||||
|
||||
val actualCordapp = loader.cordapps.single()
|
||||
assertThat(actualCordapp.contractClassNames).isEqualTo(listOf(isolatedContractId))
|
||||
assertThat(actualCordapp.initiatedFlows.single().name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Acceptor")
|
||||
assertThat(actualCordapp.initiatedFlows.first().name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Acceptor")
|
||||
assertThat(actualCordapp.rpcFlows).isEmpty()
|
||||
assertThat(actualCordapp.schedulableFlows).isEmpty()
|
||||
assertThat(actualCordapp.services).isEmpty()
|
||||
@ -74,7 +75,7 @@ class JarScanningCordappLoaderTest {
|
||||
assertThat(loader.cordapps).isNotEmpty
|
||||
|
||||
val actualCordapp = loader.cordapps.single { !it.initiatedFlows.isEmpty() }
|
||||
assertThat(actualCordapp.initiatedFlows).first().hasSameClassAs(DummyFlow::class.java)
|
||||
assertThat(actualCordapp.initiatedFlows.first()).hasSameClassAs(DummyFlow::class.java)
|
||||
assertThat(actualCordapp.rpcFlows).first().hasSameClassAs(DummyRPCFlow::class.java)
|
||||
assertThat(actualCordapp.schedulableFlows).first().hasSameClassAs(DummySchedulableFlow::class.java)
|
||||
}
|
||||
@ -142,4 +143,25 @@ class JarScanningCordappLoaderTest {
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 2))
|
||||
assertThat(loader.cordapps).hasSize(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cordapp classloader loads app signed by allowed certificate`() {
|
||||
val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!!
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), blacklistedCerts = emptyList())
|
||||
assertThat(loader.cordapps).hasSize(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cordapp classloader does not load app signed by blacklisted certificate`() {
|
||||
val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!!
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), blacklistedCerts = DEV_CERTIFICATES)
|
||||
assertThat(loader.cordapps).hasSize(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cordapp classloader loads app signed by both allowed and non-blacklisted certificate`() {
|
||||
val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-two-keys.jar")!!
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), blacklistedCerts = DEV_CERTIFICATES)
|
||||
assertThat(loader.cordapps).hasSize(1)
|
||||
}
|
||||
}
|
||||
|
@ -209,8 +209,8 @@ class NodeConfigurationImplTest {
|
||||
|
||||
val errors = configuration.validate()
|
||||
|
||||
assertThat(errors).hasOnlyOneElementSatisfying {
|
||||
error -> error.contains("Cannot configure both compatibilityZoneUrl and networkServices simultaneously")
|
||||
assertThat(errors).hasOnlyOneElementSatisfying { error ->
|
||||
error.contains("Cannot configure both compatibilityZoneUrl and networkServices simultaneously")
|
||||
}
|
||||
}
|
||||
|
||||
@ -311,7 +311,8 @@ class NodeConfigurationImplTest {
|
||||
relay = null,
|
||||
enterpriseConfiguration = EnterpriseConfiguration((MutualExclusionConfiguration(false, "", 20000, 40000))),
|
||||
crlCheckSoftFail = true,
|
||||
tlsCertCrlDistPoint = null
|
||||
tlsCertCrlDistPoint = null,
|
||||
flowOverrides = FlowOverrideConfig(listOf())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,6 @@ import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.ProgressTracker.Change
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.internal.InitiatedFlowFactory
|
||||
import net.corda.node.services.persistence.checkpoints
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.contracts.DummyState
|
||||
@ -116,7 +115,7 @@ class FlowFrameworkTests {
|
||||
|
||||
@Test
|
||||
fun `exception while fiber suspended`() {
|
||||
bobNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) }
|
||||
bobNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) }
|
||||
val flow = ReceiveFlow(bob)
|
||||
val fiber = aliceNode.services.startFlow(flow) as FlowStateMachineImpl
|
||||
// Before the flow runs change the suspend action to throw an exception
|
||||
@ -134,7 +133,7 @@ class FlowFrameworkTests {
|
||||
|
||||
@Test
|
||||
fun `both sides do a send as their first IO request`() {
|
||||
bobNode.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, 20L) }
|
||||
bobNode.registerCordappFlowFactory(PingPongFlow::class) { PingPongFlow(it, 20L) }
|
||||
aliceNode.services.startFlow(PingPongFlow(bob, 10L))
|
||||
mockNet.runNetwork()
|
||||
|
||||
@ -151,7 +150,7 @@ class FlowFrameworkTests {
|
||||
|
||||
@Test
|
||||
fun `other side ends before doing expected send`() {
|
||||
bobNode.registerFlowFactory(ReceiveFlow::class) { NoOpFlow() }
|
||||
bobNode.registerCordappFlowFactory(ReceiveFlow::class) { NoOpFlow() }
|
||||
val resultFuture = aliceNode.services.startFlow(ReceiveFlow(bob)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy {
|
||||
@ -161,7 +160,7 @@ class FlowFrameworkTests {
|
||||
|
||||
@Test
|
||||
fun `receiving unexpected session end before entering sendAndReceive`() {
|
||||
bobNode.registerFlowFactory(WaitForOtherSideEndBeforeSendAndReceive::class) { NoOpFlow() }
|
||||
bobNode.registerCordappFlowFactory(WaitForOtherSideEndBeforeSendAndReceive::class) { NoOpFlow() }
|
||||
val sessionEndReceived = Semaphore(0)
|
||||
receivedSessionMessagesObservable().filter {
|
||||
it.message is ExistingSessionMessage && it.message.payload === EndSessionMessage
|
||||
@ -176,7 +175,7 @@ class FlowFrameworkTests {
|
||||
|
||||
@Test
|
||||
fun `FlowException thrown on other side`() {
|
||||
val erroringFlow = bobNode.registerFlowFactory(ReceiveFlow::class) {
|
||||
val erroringFlow = bobNode.registerCordappFlowFactory(ReceiveFlow::class) {
|
||||
ExceptionFlow { MyFlowException("Nothing useful") }
|
||||
}
|
||||
val erroringFlowSteps = erroringFlow.flatMap { it.progressSteps }
|
||||
@ -240,7 +239,7 @@ class FlowFrameworkTests {
|
||||
}
|
||||
}
|
||||
|
||||
bobNode.registerFlowFactory(AskForExceptionFlow::class) { ConditionalExceptionFlow(it, "Hello") }
|
||||
bobNode.registerCordappFlowFactory(AskForExceptionFlow::class) { ConditionalExceptionFlow(it, "Hello") }
|
||||
val resultFuture = aliceNode.services.startFlow(RetryOnExceptionFlow(bob)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
assertThat(resultFuture.getOrThrow()).isEqualTo("Hello")
|
||||
@ -248,7 +247,7 @@ class FlowFrameworkTests {
|
||||
|
||||
@Test
|
||||
fun `serialisation issue in counterparty`() {
|
||||
bobNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(NonSerialisableData(1), it) }
|
||||
bobNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(NonSerialisableData(1), it) }
|
||||
val result = aliceNode.services.startFlow(ReceiveFlow(bob)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy {
|
||||
@ -258,7 +257,7 @@ class FlowFrameworkTests {
|
||||
|
||||
@Test
|
||||
fun `FlowException has non-serialisable object`() {
|
||||
bobNode.registerFlowFactory(ReceiveFlow::class) {
|
||||
bobNode.registerCordappFlowFactory(ReceiveFlow::class) {
|
||||
ExceptionFlow { NonSerialisableFlowException(NonSerialisableData(1)) }
|
||||
}
|
||||
val result = aliceNode.services.startFlow(ReceiveFlow(bob)).resultFuture
|
||||
@ -275,7 +274,7 @@ class FlowFrameworkTests {
|
||||
.addCommand(dummyCommand(alice.owningKey))
|
||||
val stx = aliceNode.services.signInitialTransaction(ptx)
|
||||
|
||||
val committerFiber = aliceNode.registerFlowFactory(WaitingFlows.Waiter::class) {
|
||||
val committerFiber = aliceNode.registerCordappFlowFactory(WaitingFlows.Waiter::class) {
|
||||
WaitingFlows.Committer(it)
|
||||
}.map { it.stateMachine }.map { uncheckedCast<FlowStateMachine<*>, FlowStateMachine<Any>>(it) }
|
||||
val waiterStx = bobNode.services.startFlow(WaitingFlows.Waiter(stx, alice)).resultFuture
|
||||
@ -290,7 +289,7 @@ class FlowFrameworkTests {
|
||||
.addCommand(dummyCommand())
|
||||
val stx = aliceNode.services.signInitialTransaction(ptx)
|
||||
|
||||
aliceNode.registerFlowFactory(WaitingFlows.Waiter::class) {
|
||||
aliceNode.registerCordappFlowFactory(WaitingFlows.Waiter::class) {
|
||||
WaitingFlows.Committer(it) { throw Exception("Error") }
|
||||
}
|
||||
val waiter = bobNode.services.startFlow(WaitingFlows.Waiter(stx, alice)).resultFuture
|
||||
@ -307,7 +306,7 @@ class FlowFrameworkTests {
|
||||
.addCommand(dummyCommand(alice.owningKey))
|
||||
val stx = aliceNode.services.signInitialTransaction(ptx)
|
||||
|
||||
aliceNode.registerFlowFactory(VaultQueryFlow::class) {
|
||||
aliceNode.registerCordappFlowFactory(VaultQueryFlow::class) {
|
||||
WaitingFlows.Committer(it)
|
||||
}
|
||||
val result = bobNode.services.startFlow(VaultQueryFlow(stx, alice)).resultFuture
|
||||
@ -318,7 +317,7 @@ class FlowFrameworkTests {
|
||||
|
||||
@Test
|
||||
fun `customised client flow`() {
|
||||
val receiveFlowFuture = bobNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it) }
|
||||
val receiveFlowFuture = bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it) }
|
||||
aliceNode.services.startFlow(CustomSendFlow("Hello", bob)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
assertThat(receiveFlowFuture.getOrThrow().receivedPayloads).containsOnly("Hello")
|
||||
@ -333,7 +332,7 @@ class FlowFrameworkTests {
|
||||
|
||||
@Test
|
||||
fun `upgraded initiating flow`() {
|
||||
bobNode.registerFlowFactory(UpgradedFlow::class, initiatedFlowVersion = 1) { InitiatedSendFlow("Old initiated", it) }
|
||||
bobNode.registerCordappFlowFactory(UpgradedFlow::class, initiatedFlowVersion = 1) { InitiatedSendFlow("Old initiated", it) }
|
||||
val result = aliceNode.services.startFlow(UpgradedFlow(bob)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
assertThat(receivedSessionMessages).startsWith(
|
||||
@ -347,7 +346,7 @@ class FlowFrameworkTests {
|
||||
|
||||
@Test
|
||||
fun `upgraded initiated flow`() {
|
||||
bobNode.registerFlowFactory(SendFlow::class, initiatedFlowVersion = 2) { UpgradedFlow(it) }
|
||||
bobNode.registerCordappFlowFactory(SendFlow::class, initiatedFlowVersion = 2) { UpgradedFlow(it) }
|
||||
val initiatingFlow = SendFlow("Old initiating", bob)
|
||||
val flowInfo = aliceNode.services.startFlow(initiatingFlow).resultFuture
|
||||
mockNet.runNetwork()
|
||||
@ -387,7 +386,7 @@ class FlowFrameworkTests {
|
||||
|
||||
@Test
|
||||
fun `single inlined sub-flow`() {
|
||||
bobNode.registerFlowFactory(SendAndReceiveFlow::class) { SingleInlinedSubFlow(it) }
|
||||
bobNode.registerCordappFlowFactory(SendAndReceiveFlow::class) { SingleInlinedSubFlow(it) }
|
||||
val result = aliceNode.services.startFlow(SendAndReceiveFlow(bob, "Hello")).resultFuture
|
||||
mockNet.runNetwork()
|
||||
assertThat(result.getOrThrow()).isEqualTo("HelloHello")
|
||||
@ -395,7 +394,7 @@ class FlowFrameworkTests {
|
||||
|
||||
@Test
|
||||
fun `double inlined sub-flow`() {
|
||||
bobNode.registerFlowFactory(SendAndReceiveFlow::class) { DoubleInlinedSubFlow(it) }
|
||||
bobNode.registerCordappFlowFactory(SendAndReceiveFlow::class) { DoubleInlinedSubFlow(it) }
|
||||
val result = aliceNode.services.startFlow(SendAndReceiveFlow(bob, "Hello")).resultFuture
|
||||
mockNet.runNetwork()
|
||||
assertThat(result.getOrThrow()).isEqualTo("HelloHello")
|
||||
@ -403,7 +402,7 @@ class FlowFrameworkTests {
|
||||
|
||||
@Test
|
||||
fun `non-FlowException thrown on other side`() {
|
||||
val erroringFlowFuture = bobNode.registerFlowFactory(ReceiveFlow::class) {
|
||||
val erroringFlowFuture = bobNode.registerCordappFlowFactory(ReceiveFlow::class) {
|
||||
ExceptionFlow { Exception("evil bug!") }
|
||||
}
|
||||
val erroringFlowSteps = erroringFlowFuture.flatMap { it.progressSteps }
|
||||
@ -507,8 +506,8 @@ class FlowFrameworkTripartyTests {
|
||||
|
||||
@Test
|
||||
fun `sending to multiple parties`() {
|
||||
bobNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() }
|
||||
charlieNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() }
|
||||
bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() }
|
||||
charlieNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() }
|
||||
val payload = "Hello World"
|
||||
aliceNode.services.startFlow(SendFlow(payload, bob, charlie))
|
||||
mockNet.runNetwork()
|
||||
@ -538,8 +537,8 @@ class FlowFrameworkTripartyTests {
|
||||
fun `receiving from multiple parties`() {
|
||||
val bobPayload = "Test 1"
|
||||
val charliePayload = "Test 2"
|
||||
bobNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(bobPayload, it) }
|
||||
charlieNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(charliePayload, it) }
|
||||
bobNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(bobPayload, it) }
|
||||
charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(charliePayload, it) }
|
||||
val multiReceiveFlow = ReceiveFlow(bob, charlie).nonTerminating()
|
||||
aliceNode.services.startFlow(multiReceiveFlow)
|
||||
aliceNode.internals.acceptableLiveFiberCountOnStop = 1
|
||||
@ -564,8 +563,8 @@ class FlowFrameworkTripartyTests {
|
||||
|
||||
@Test
|
||||
fun `FlowException only propagated to parent`() {
|
||||
charlieNode.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } }
|
||||
bobNode.registerFlowFactory(ReceiveFlow::class) { ReceiveFlow(charlie) }
|
||||
charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } }
|
||||
bobNode.registerCordappFlowFactory(ReceiveFlow::class) { ReceiveFlow(charlie) }
|
||||
val receivingFiber = aliceNode.services.startFlow(ReceiveFlow(bob))
|
||||
mockNet.runNetwork()
|
||||
assertThatExceptionOfType(UnexpectedFlowEndException::class.java)
|
||||
@ -577,9 +576,9 @@ class FlowFrameworkTripartyTests {
|
||||
// Bob will send its payload and then block waiting for the receive from Alice. Meanwhile Alice will move
|
||||
// onto Charlie which will throw the exception
|
||||
val node2Fiber = bobNode
|
||||
.registerFlowFactory(ReceiveFlow::class) { SendAndReceiveFlow(it, "Hello") }
|
||||
.registerCordappFlowFactory(ReceiveFlow::class) { SendAndReceiveFlow(it, "Hello") }
|
||||
.map { it.stateMachine }
|
||||
charlieNode.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } }
|
||||
charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } }
|
||||
|
||||
val aliceFiber = aliceNode.services.startFlow(ReceiveFlow(bob, charlie)) as FlowStateMachineImpl
|
||||
mockNet.runNetwork()
|
||||
@ -630,6 +629,8 @@ class FlowFrameworkPersistenceTests {
|
||||
private lateinit var notaryIdentity: Party
|
||||
private lateinit var alice: Party
|
||||
private lateinit var bob: Party
|
||||
private lateinit var aliceFlowManager: MockNodeFlowManager
|
||||
private lateinit var bobFlowManager: MockNodeFlowManager
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
@ -637,8 +638,11 @@ class FlowFrameworkPersistenceTests {
|
||||
cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"),
|
||||
servicePeerAllocationStrategy = RoundRobin()
|
||||
)
|
||||
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME))
|
||||
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME))
|
||||
aliceFlowManager = MockNodeFlowManager()
|
||||
bobFlowManager = MockNodeFlowManager()
|
||||
|
||||
aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME, flowManager = aliceFlowManager))
|
||||
bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, flowManager = bobFlowManager))
|
||||
|
||||
receivedSessionMessagesObservable().forEach { receivedSessionMessages += it }
|
||||
|
||||
@ -664,7 +668,7 @@ class FlowFrameworkPersistenceTests {
|
||||
|
||||
@Test
|
||||
fun `flow restarted just after receiving payload`() {
|
||||
bobNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() }
|
||||
bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() }
|
||||
aliceNode.services.startFlow(SendFlow("Hello", bob))
|
||||
|
||||
// We push through just enough messages to get only the payload sent
|
||||
@ -679,7 +683,7 @@ class FlowFrameworkPersistenceTests {
|
||||
|
||||
@Test
|
||||
fun `flow loaded from checkpoint will respond to messages from before start`() {
|
||||
aliceNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) }
|
||||
aliceNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) }
|
||||
bobNode.services.startFlow(ReceiveFlow(alice).nonTerminating()) // Prepare checkpointed receive flow
|
||||
val restoredFlow = bobNode.restartAndGetRestoredFlow<ReceiveFlow>()
|
||||
assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello")
|
||||
@ -694,7 +698,7 @@ class FlowFrameworkPersistenceTests {
|
||||
var sentCount = 0
|
||||
mockNet.messagingNetwork.sentMessages.toSessionTransfers().filter { it.isPayloadTransfer }.forEach { sentCount++ }
|
||||
val charlieNode = mockNet.createNode(InternalMockNodeParameters(legalName = CHARLIE_NAME))
|
||||
val secondFlow = charlieNode.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) }
|
||||
val secondFlow = charlieNode.registerCordappFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) }
|
||||
mockNet.runNetwork()
|
||||
val charlie = charlieNode.info.singleIdentity()
|
||||
|
||||
@ -802,23 +806,14 @@ private infix fun TestStartedNode.sent(message: SessionMessage): Pair<Int, Sessi
|
||||
private infix fun Pair<Int, SessionMessage>.to(node: TestStartedNode): SessionTransfer = SessionTransfer(first, second, node.network.myAddress)
|
||||
|
||||
private data class SessionTransfer(val from: Int, val message: SessionMessage, val to: MessageRecipients) {
|
||||
val isPayloadTransfer: Boolean get() =
|
||||
message is ExistingSessionMessage && message.payload is DataSessionMessage ||
|
||||
message is InitialSessionMessage && message.firstPayload != null
|
||||
val isPayloadTransfer: Boolean
|
||||
get() =
|
||||
message is ExistingSessionMessage && message.payload is DataSessionMessage ||
|
||||
message is InitialSessionMessage && message.firstPayload != null
|
||||
|
||||
override fun toString(): String = "$from sent $message to $to"
|
||||
}
|
||||
|
||||
private inline fun <reified P : FlowLogic<*>> TestStartedNode.registerFlowFactory(
|
||||
initiatingFlowClass: KClass<out FlowLogic<*>>,
|
||||
initiatedFlowVersion: Int = 1,
|
||||
noinline flowFactory: (FlowSession) -> P): CordaFuture<P> {
|
||||
val observable = registerFlowFactory(
|
||||
initiatingFlowClass.java,
|
||||
InitiatedFlowFactory.CorDapp(initiatedFlowVersion, "", flowFactory),
|
||||
P::class.java,
|
||||
track = true)
|
||||
return observable.toFuture()
|
||||
}
|
||||
|
||||
private fun sessionInit(clientFlowClass: KClass<out FlowLogic<*>>, flowVersion: Int = 1, payload: Any? = null): InitialSessionMessage {
|
||||
return InitialSessionMessage(SessionId(0), 0, clientFlowClass.java.name, flowVersion, "", payload?.serialize())
|
||||
@ -1061,7 +1056,8 @@ private class SendAndReceiveFlow(val otherParty: Party, val payload: Any, val ot
|
||||
constructor(otherPartySession: FlowSession, payload: Any) : this(otherPartySession.counterparty, payload, otherPartySession)
|
||||
|
||||
@Suspendable
|
||||
override fun call(): Any = (otherPartySession ?: initiateFlow(otherParty)).sendAndReceive<Any>(payload).unwrap { it }
|
||||
override fun call(): Any = (otherPartySession
|
||||
?: initiateFlow(otherParty)).sendAndReceive<Any>(payload).unwrap { it }
|
||||
}
|
||||
|
||||
private class InlinedSendFlow(val payload: String, val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
@ -1098,4 +1094,4 @@ private class ExceptionFlow<E : Exception>(val exception: () -> E) : FlowLogic<N
|
||||
exceptionThrown = exception()
|
||||
throw exceptionThrown
|
||||
}
|
||||
}
|
||||
}
|
@ -3,10 +3,7 @@ package net.corda.node.services.vault
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.nhaarman.mockito_kotlin.*
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.internal.FlowStateMachine
|
||||
import net.corda.core.internal.packageName
|
||||
@ -23,14 +20,12 @@ import net.corda.core.utilities.NonEmptySet
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.internal.InitiatedFlowFactory
|
||||
import net.corda.node.services.api.SchemaService
|
||||
import net.corda.node.services.api.VaultServiceInternal
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.node.internal.cordappsForPackages
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.internal.cordappsForPackages
|
||||
import net.corda.testing.node.internal.startFlow
|
||||
import org.junit.After
|
||||
import org.junit.Test
|
||||
@ -68,7 +63,7 @@ class NodePair(private val mockNet: InternalMockNetwork) {
|
||||
private set
|
||||
|
||||
fun <T> communicate(clientLogic: AbstractClientLogic<T>, rebootClient: Boolean): FlowStateMachine<T> {
|
||||
server.registerFlowFactory(AbstractClientLogic::class.java, InitiatedFlowFactory.Core { ServerLogic(it, serverRunning) }, ServerLogic::class.java, false)
|
||||
server.registerCoreFlowFactory(AbstractClientLogic::class.java, ServerLogic::class.java, { ServerLogic(it, serverRunning) }, false)
|
||||
client.services.startFlow(clientLogic)
|
||||
while (!serverRunning.get()) mockNet.runNetwork(1)
|
||||
if (rebootClient) {
|
||||
|
Binary file not shown.
Binary file not shown.
@ -56,9 +56,11 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask,
|
||||
webPort 10007
|
||||
rpcUsers = [[user: "bankUser", password: "test", permissions: ["ALL"]]]
|
||||
extraConfig = [
|
||||
custom : [issuableCurrencies: ["USD"]],
|
||||
h2Settings: [address: "localhost:10017"]
|
||||
]
|
||||
cordapp(project(':finance')) {
|
||||
config "issuableCurrencies = [ USD ]"
|
||||
}
|
||||
}
|
||||
node {
|
||||
name "O=BigCorporation,L=New York,C=US"
|
||||
|
@ -89,6 +89,20 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask,
|
||||
}
|
||||
extraConfig = ['h2Settings.address' : 'localhost:10017']
|
||||
}
|
||||
|
||||
//All other nodes should be using LoggingBuyerFlow as it is a subclass of BuyerFlow
|
||||
node {
|
||||
name "O=LoggingBank,L=London,C=GB"
|
||||
p2pPort 10025
|
||||
cordapps = ["$project.group:finance:$corda_release_version"]
|
||||
rpcUsers = ext.rpcUsers
|
||||
rpcSettings {
|
||||
address "localhost:10026"
|
||||
adminAddress "localhost:10027"
|
||||
}
|
||||
extraConfig = ['h2Settings.address' : 'localhost:10035']
|
||||
flowOverride("net.corda.traderdemo.flow.SellerFlow", "net.corda.traderdemo.flow.BuyerFlow")
|
||||
}
|
||||
}
|
||||
|
||||
task integrationTest(type: Test, dependsOn: []) {
|
||||
|
@ -11,20 +11,17 @@ import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.finance.contracts.CommercialPaper
|
||||
import net.corda.finance.contracts.getCashBalances
|
||||
import net.corda.finance.flows.TwoPartyTradeFlow
|
||||
import net.corda.traderdemo.TransactionGraphSearch
|
||||
import java.util.*
|
||||
|
||||
@InitiatedBy(SellerFlow::class)
|
||||
class BuyerFlow(private val otherSideSession: FlowSession) : FlowLogic<Unit>() {
|
||||
open class BuyerFlow(private val otherSideSession: FlowSession) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
object STARTING_BUY : ProgressTracker.Step("Seller connected, purchasing commercial paper asset")
|
||||
|
||||
override val progressTracker: ProgressTracker = ProgressTracker(STARTING_BUY)
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
override fun call(): SignedTransaction {
|
||||
progressTracker.currentStep = STARTING_BUY
|
||||
|
||||
// Receive the offered amount and automatically agree to it (in reality this would be a longer negotiation)
|
||||
@ -43,33 +40,6 @@ class BuyerFlow(private val otherSideSession: FlowSession) : FlowLogic<Unit>() {
|
||||
println("Purchase complete - we are a happy customer! Final transaction is: " +
|
||||
"\n\n${Emoji.renderIfSupported(tradeTX.tx)}")
|
||||
|
||||
logIssuanceAttachment(tradeTX)
|
||||
logBalance()
|
||||
}
|
||||
|
||||
private fun logBalance() {
|
||||
val balances = serviceHub.getCashBalances().entries.map { "${it.key.currencyCode} ${it.value}" }
|
||||
println("Remaining balance: ${balances.joinToString()}")
|
||||
}
|
||||
|
||||
private fun logIssuanceAttachment(tradeTX: SignedTransaction) {
|
||||
// Find the original CP issuance.
|
||||
// TODO: This is potentially very expensive, and requires transaction details we may no longer have once
|
||||
// SGX is enabled. Should be replaced with including the attachment on all transactions involving
|
||||
// the state.
|
||||
val search = TransactionGraphSearch(serviceHub.validatedTransactions, listOf(tradeTX.tx),
|
||||
TransactionGraphSearch.Query(withCommandOfType = CommercialPaper.Commands.Issue::class.java,
|
||||
followInputsOfType = CommercialPaper.State::class.java))
|
||||
val cpIssuance = search.call().single()
|
||||
|
||||
// Buyer will fetch the attachment from the seller automatically when it resolves the transaction.
|
||||
|
||||
cpIssuance.attachments.first().let {
|
||||
println("""
|
||||
|
||||
The issuance of the commercial paper came with an attachment. You can find it in the attachments directory: $it.jar
|
||||
|
||||
${Emoji.renderIfSupported(cpIssuance)}""")
|
||||
}
|
||||
return tradeTX
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,48 @@
|
||||
package net.corda.traderdemo.flow
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.finance.contracts.CommercialPaper
|
||||
import net.corda.finance.contracts.getCashBalances
|
||||
import net.corda.traderdemo.TransactionGraphSearch
|
||||
|
||||
@InitiatedBy(SellerFlow::class)
|
||||
class LoggingBuyerFlow(private val otherSideSession: FlowSession) : BuyerFlow(otherSideSession) {
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val tradeTX = super.call()
|
||||
logIssuanceAttachment(tradeTX)
|
||||
logBalance()
|
||||
return tradeTX
|
||||
}
|
||||
|
||||
private fun logBalance() {
|
||||
val balances = serviceHub.getCashBalances().entries.map { "${it.key.currencyCode} ${it.value}" }
|
||||
println("Remaining balance: ${balances.joinToString()}")
|
||||
}
|
||||
|
||||
private fun logIssuanceAttachment(tradeTX: SignedTransaction) {
|
||||
// Find the original CP issuance.
|
||||
// TODO: This is potentially very expensive, and requires transaction details we may no longer have once
|
||||
// SGX is enabled. Should be replaced with including the attachment on all transactions involving
|
||||
// the state.
|
||||
val search = TransactionGraphSearch(serviceHub.validatedTransactions, listOf(tradeTX.tx),
|
||||
TransactionGraphSearch.Query(withCommandOfType = CommercialPaper.Commands.Issue::class.java,
|
||||
followInputsOfType = CommercialPaper.State::class.java))
|
||||
val cpIssuance = search.call().single()
|
||||
|
||||
// Buyer will fetch the attachment from the seller automatically when it resolves the transaction.
|
||||
|
||||
cpIssuance.attachments.first().let {
|
||||
println("""
|
||||
|
||||
The issuance of the commercial paper came with an attachment. You can find it in the attachments directory: $it.jar
|
||||
|
||||
${Emoji.renderIfSupported(cpIssuance)}""")
|
||||
}
|
||||
}
|
||||
}
|
@ -148,7 +148,8 @@ data class NodeParameters(
|
||||
val maximumHeapSize: String = "512m",
|
||||
val logLevel: String? = null,
|
||||
val additionalCordapps: Collection<TestCordapp> = emptySet(),
|
||||
val regenerateCordappsOnStart: Boolean = false
|
||||
val regenerateCordappsOnStart: Boolean = false,
|
||||
val flowOverrides: Map<Class<out FlowLogic<*>>, Class<out FlowLogic<*>>> = emptyMap()
|
||||
) {
|
||||
/**
|
||||
* Helper builder for configuring a [Node] from Java.
|
||||
|
@ -2,6 +2,7 @@ package net.corda.testing.driver
|
||||
|
||||
import net.corda.core.DoNotImplement
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.concurrent.map
|
||||
@ -27,13 +28,14 @@ interface DriverDSL {
|
||||
* Returns the [NotaryHandle] for the single notary on the network. Throws if there are none or more than one.
|
||||
* @see notaryHandles
|
||||
*/
|
||||
val defaultNotaryHandle: NotaryHandle get() {
|
||||
return when (notaryHandles.size) {
|
||||
0 -> throw IllegalStateException("There are no notaries defined on the network")
|
||||
1 -> notaryHandles[0]
|
||||
else -> throw IllegalStateException("There is more than one notary defined on the network")
|
||||
val defaultNotaryHandle: NotaryHandle
|
||||
get() {
|
||||
return when (notaryHandles.size) {
|
||||
0 -> throw IllegalStateException("There are no notaries defined on the network")
|
||||
1 -> notaryHandles[0]
|
||||
else -> throw IllegalStateException("There is more than one notary defined on the network")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the identity of the single notary on the network. Throws if there are none or more than one.
|
||||
@ -47,11 +49,12 @@ interface DriverDSL {
|
||||
* @see defaultNotaryHandle
|
||||
* @see notaryHandles
|
||||
*/
|
||||
val defaultNotaryNode: CordaFuture<NodeHandle> get() {
|
||||
return defaultNotaryHandle.nodeHandles.map {
|
||||
it.singleOrNull() ?: throw IllegalStateException("Default notary is not a single node")
|
||||
val defaultNotaryNode: CordaFuture<NodeHandle>
|
||||
get() {
|
||||
return defaultNotaryHandle.nodeHandles.map {
|
||||
it.singleOrNull() ?: throw IllegalStateException("Default notary is not a single node")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a node.
|
||||
@ -110,7 +113,8 @@ interface DriverDSL {
|
||||
startInSameProcess: Boolean? = defaultParameters.startInSameProcess,
|
||||
maximumHeapSize: String = defaultParameters.maximumHeapSize,
|
||||
additionalCordapps: Collection<TestCordapp> = defaultParameters.additionalCordapps,
|
||||
regenerateCordappsOnStart: Boolean = defaultParameters.regenerateCordappsOnStart
|
||||
regenerateCordappsOnStart: Boolean = defaultParameters.regenerateCordappsOnStart,
|
||||
flowOverrides: Map<out Class<out FlowLogic<*>>, Class<out FlowLogic<*>>> = defaultParameters.flowOverrides
|
||||
): CordaFuture<NodeHandle>
|
||||
|
||||
/**
|
||||
|
@ -14,6 +14,7 @@ import net.corda.testing.driver.OutOfProcess
|
||||
import net.corda.testing.node.User
|
||||
import rx.Observable
|
||||
import java.nio.file.Path
|
||||
import javax.validation.constraints.NotNull
|
||||
|
||||
interface NodeHandleInternal : NodeHandle {
|
||||
val configuration: NodeConfiguration
|
||||
@ -70,7 +71,11 @@ data class InProcessImpl(
|
||||
}
|
||||
|
||||
override fun close() = stop()
|
||||
override fun <T : FlowLogic<*>> registerInitiatedFlow(initiatedFlowClass: Class<T>): Observable<T> = node.registerInitiatedFlow(initiatedFlowClass)
|
||||
@NotNull
|
||||
override fun <T : FlowLogic<*>> registerInitiatedFlow(initiatedFlowClass: Class<T>): Observable<T> {
|
||||
node.registerInitiatedFlow(initiatedFlowClass)
|
||||
return Observable.empty()
|
||||
}
|
||||
}
|
||||
|
||||
val InProcess.internalServices: StartedNodeServices get() = services as StartedNodeServices
|
||||
|
@ -206,11 +206,8 @@ class StartedMockNode private constructor(private val node: TestStartedNode) {
|
||||
fun <F : FlowLogic<*>> registerResponderFlow(initiatingFlowClass: Class<out FlowLogic<*>>,
|
||||
flowFactory: ResponderFlowFactory<F>,
|
||||
responderFlowClass: Class<F>): CordaFuture<F> =
|
||||
node.registerFlowFactory(
|
||||
initiatingFlowClass,
|
||||
InitiatedFlowFactory.CorDapp(flowVersion = 0, appName = "", factory = flowFactory::invoke),
|
||||
responderFlowClass, true)
|
||||
.toFuture()
|
||||
|
||||
node.registerInitiatedFlow(initiatingFlowClass, responderFlowClass).toFuture()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -240,13 +237,12 @@ interface ResponderFlowFactory<F : FlowLogic<*>> {
|
||||
*/
|
||||
inline fun <reified F : FlowLogic<*>> StartedMockNode.registerResponderFlow(
|
||||
initiatingFlowClass: Class<out FlowLogic<*>>,
|
||||
noinline flowFactory: (FlowSession) -> F): Future<F> =
|
||||
registerResponderFlow(
|
||||
initiatingFlowClass,
|
||||
object : ResponderFlowFactory<F> {
|
||||
override fun invoke(flowSession: FlowSession) = flowFactory(flowSession)
|
||||
},
|
||||
F::class.java)
|
||||
noinline flowFactory: (FlowSession) -> F): Future<F> = registerResponderFlow(
|
||||
initiatingFlowClass,
|
||||
object : ResponderFlowFactory<F> {
|
||||
override fun invoke(flowSession: FlowSession) = flowFactory(flowSession)
|
||||
},
|
||||
F::class.java)
|
||||
|
||||
/**
|
||||
* A mock node brings up a suite of in-memory services in a fast manner suitable for unit testing.
|
||||
@ -302,13 +298,13 @@ open class MockNetwork(
|
||||
constructor(cordappPackages: List<String>, parameters: MockNetworkParameters = MockNetworkParameters()) : this(cordappPackages, defaultParameters = parameters)
|
||||
|
||||
constructor(
|
||||
cordappPackages: List<String>,
|
||||
defaultParameters: MockNetworkParameters = MockNetworkParameters(),
|
||||
networkSendManuallyPumped: Boolean = defaultParameters.networkSendManuallyPumped,
|
||||
threadPerNode: Boolean = defaultParameters.threadPerNode,
|
||||
servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = defaultParameters.servicePeerAllocationStrategy,
|
||||
notarySpecs: List<MockNetworkNotarySpec> = defaultParameters.notarySpecs,
|
||||
networkParameters: NetworkParameters = defaultParameters.networkParameters
|
||||
cordappPackages: List<String>,
|
||||
defaultParameters: MockNetworkParameters = MockNetworkParameters(),
|
||||
networkSendManuallyPumped: Boolean = defaultParameters.networkSendManuallyPumped,
|
||||
threadPerNode: Boolean = defaultParameters.threadPerNode,
|
||||
servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = defaultParameters.servicePeerAllocationStrategy,
|
||||
notarySpecs: List<MockNetworkNotarySpec> = defaultParameters.notarySpecs,
|
||||
networkParameters: NetworkParameters = defaultParameters.networkParameters
|
||||
) : this(emptyList(), defaultParameters, networkSendManuallyPumped, threadPerNode, servicePeerAllocationStrategy, notarySpecs, networkParameters, cordappsForAllNodes = cordappsForPackages(cordappPackages))
|
||||
|
||||
private val internalMockNetwork: InternalMockNetwork = InternalMockNetwork(defaultParameters, networkSendManuallyPumped, threadPerNode, servicePeerAllocationStrategy, notarySpecs, networkParameters = networkParameters, cordappsForAllNodes = cordappsForAllNodes)
|
||||
|
@ -19,12 +19,15 @@ interface TestCordapp {
|
||||
/** Returns the version string, defaults to "1.0" if not specified. */
|
||||
val version: String
|
||||
|
||||
/** Returns the vendor string, defaults to "Corda" if not specified. */
|
||||
/** Returns the vendor string, defaults to "test-vendor" if not specified. */
|
||||
val vendor: String
|
||||
|
||||
/** Returns the target platform version, defaults to the current platform version if not specified. */
|
||||
val targetVersion: Int
|
||||
|
||||
/** Returns the config for this CorDapp, defaults to empty if not specified. */
|
||||
val config: Map<String, Any>
|
||||
|
||||
/** Returns the set of package names scanned for this test CorDapp. */
|
||||
val packages: Set<String>
|
||||
|
||||
@ -43,6 +46,9 @@ interface TestCordapp {
|
||||
/** Return a copy of this [TestCordapp] but with the specified target platform version. */
|
||||
fun withTargetVersion(targetVersion: Int): TestCordapp
|
||||
|
||||
/** Returns a copy of this [TestCordapp] but with the specified CorDapp config. */
|
||||
fun withConfig(config: Map<String, Any>): TestCordapp
|
||||
|
||||
class Factory {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@ -57,9 +63,10 @@ interface TestCordapp {
|
||||
return TestCordappImpl(
|
||||
name = "test-cordapp",
|
||||
version = "1.0",
|
||||
vendor = "Corda",
|
||||
vendor = "test-vendor",
|
||||
title = "test-title",
|
||||
targetVersion = PLATFORM_VERSION,
|
||||
config = emptyMap(),
|
||||
packages = simplifyScanPackages(packageNames),
|
||||
classes = emptySet()
|
||||
)
|
||||
|
@ -8,6 +8,7 @@ import com.typesafe.config.ConfigValueFactory
|
||||
import net.corda.client.rpc.internal.createCordaRPCClientWithSslAndClassLoader
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.concurrent.firstOf
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.concurrent.*
|
||||
@ -210,22 +211,35 @@ class DriverDSLImpl(
|
||||
maximumHeapSize,
|
||||
defaultParameters.additionalCordapps,
|
||||
defaultParameters.regenerateCordappsOnStart,
|
||||
emptyMap(),
|
||||
null
|
||||
)
|
||||
}
|
||||
override fun startNode(defaultParameters: NodeParameters,
|
||||
providedName: CordaX500Name?,
|
||||
rpcUsers: List<User>,
|
||||
verifierType: VerifierType,
|
||||
customOverrides: Map<String, Any?>,
|
||||
startInSameProcess: Boolean?,
|
||||
maximumHeapSize: String,
|
||||
additionalCordapps: Collection<TestCordapp>,
|
||||
regenerateCordappsOnStart: Boolean,
|
||||
flowOverrides: Map<out Class<out FlowLogic<*>>, Class<out FlowLogic<*>>>): CordaFuture<NodeHandle> {
|
||||
return startNode(
|
||||
defaultParameters,
|
||||
providedName,
|
||||
rpcUsers,
|
||||
verifierType,
|
||||
customOverrides,
|
||||
startInSameProcess,
|
||||
maximumHeapSize,
|
||||
defaultParameters.additionalCordapps,
|
||||
defaultParameters.regenerateCordappsOnStart,
|
||||
flowOverrides,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
override fun startNode(
|
||||
defaultParameters: NodeParameters,
|
||||
providedName: CordaX500Name?,
|
||||
rpcUsers: List<User>,
|
||||
verifierType: VerifierType,
|
||||
customOverrides: Map<String, Any?>,
|
||||
startInSameProcess: Boolean?,
|
||||
maximumHeapSize: String,
|
||||
additionalCordapps: Collection<TestCordapp>,
|
||||
regenerateCordappsOnStart: Boolean
|
||||
) = startNode(defaultParameters, providedName, rpcUsers, verifierType, customOverrides, startInSameProcess, maximumHeapSize, additionalCordapps, regenerateCordappsOnStart, null)
|
||||
|
||||
override fun startNode(
|
||||
defaultParameters: NodeParameters,
|
||||
providedName: CordaX500Name?,
|
||||
@ -236,6 +250,7 @@ class DriverDSLImpl(
|
||||
maximumHeapSize: String,
|
||||
additionalCordapps: Collection<TestCordapp>,
|
||||
regenerateCordappsOnStart: Boolean,
|
||||
flowOverrides: Map<out Class<out FlowLogic<*>>, Class<out FlowLogic<*>>>,
|
||||
bytemanPort: Int?
|
||||
): CordaFuture<NodeHandle> {
|
||||
val p2pAddress = portAllocation.nextHostAndPort()
|
||||
@ -253,7 +268,7 @@ class DriverDSLImpl(
|
||||
return registrationFuture.flatMap {
|
||||
networkMapAvailability.flatMap {
|
||||
// But starting the node proper does require the network map
|
||||
startRegisteredNode(name, it, rpcUsers, verifierType, customOverrides, startInSameProcess, maximumHeapSize, p2pAddress, additionalCordapps, regenerateCordappsOnStart, bytemanPort)
|
||||
startRegisteredNode(name, it, rpcUsers, verifierType, customOverrides, startInSameProcess, maximumHeapSize, p2pAddress, additionalCordapps, regenerateCordappsOnStart, flowOverrides, bytemanPort)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -268,6 +283,7 @@ class DriverDSLImpl(
|
||||
p2pAddress: NetworkHostAndPort = portAllocation.nextHostAndPort(),
|
||||
additionalCordapps: Collection<TestCordapp> = emptySet(),
|
||||
regenerateCordappsOnStart: Boolean = false,
|
||||
flowOverrides: Map<out Class<out FlowLogic<*>>, Class<out FlowLogic<*>>> = emptyMap(),
|
||||
bytemanPort: Int? = null): CordaFuture<NodeHandle> {
|
||||
val rpcAddress = portAllocation.nextHostAndPort()
|
||||
val rpcAdminAddress = portAllocation.nextHostAndPort()
|
||||
@ -282,15 +298,17 @@ class DriverDSLImpl(
|
||||
"networkServices.networkMapURL" to compatibilityZone.networkMapURL().toString())
|
||||
}
|
||||
|
||||
val flowOverrideConfig = flowOverrides.entries.map { FlowOverride(it.key.canonicalName, it.value.canonicalName) }.let { FlowOverrideConfig(it) }
|
||||
val overrides = configOf(
|
||||
"myLegalName" to name.toString(),
|
||||
"p2pAddress" to p2pAddress.toString(),
|
||||
NodeConfiguration::myLegalName.name to name.toString(),
|
||||
NodeConfiguration::p2pAddress.name to p2pAddress.toString(),
|
||||
"rpcSettings.address" to rpcAddress.toString(),
|
||||
"rpcSettings.adminAddress" to rpcAdminAddress.toString(),
|
||||
"useTestClock" to useTestClock,
|
||||
"rpcUsers" to if (users.isEmpty()) defaultRpcUserList else users.map { it.toConfig().root().unwrapped() },
|
||||
"verifierType" to verifierType.name,
|
||||
"enterpriseConfiguration.tuning.flowThreadPoolSize" to "1"
|
||||
NodeConfiguration::useTestClock.name to useTestClock,
|
||||
NodeConfiguration::rpcUsers.name to if (users.isEmpty()) defaultRpcUserList else users.map { it.toConfig().root().unwrapped() },
|
||||
NodeConfiguration::verifierType.name to verifierType.name,
|
||||
"enterpriseConfiguration.tuning.flowThreadPoolSize" to "1",
|
||||
NodeConfiguration::flowOverrides.name to flowOverrideConfig.toConfig().root().unwrapped()
|
||||
) + czUrlConfig + customOverrides
|
||||
val config = NodeConfig(ConfigHelper.loadConfig(
|
||||
baseDirectory = baseDirectory(name),
|
||||
@ -342,7 +360,7 @@ class DriverDSLImpl(
|
||||
} else {
|
||||
startOutOfProcessMiniNode(
|
||||
config,
|
||||
"--initial-registration",
|
||||
"initial-registration",
|
||||
"--network-root-truststore=${rootTruststorePath.toAbsolutePath()}",
|
||||
"--network-root-truststore-password=$rootTruststorePassword"
|
||||
).map { config }
|
||||
@ -477,7 +495,7 @@ class DriverDSLImpl(
|
||||
} else {
|
||||
// TODO The config we use here is uses a hardocded p2p port which changes when the node is run proper
|
||||
// This causes two node info files to be generated.
|
||||
startOutOfProcessMiniNode(config, "--just-generate-node-info").map {
|
||||
startOutOfProcessMiniNode(config, "generate-node-info").map {
|
||||
// Once done we have to read the signed node info file that's been generated
|
||||
val nodeInfoFile = config.corda.baseDirectory.list { paths ->
|
||||
paths.filter { it.fileName.toString().startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get()
|
||||
@ -541,8 +559,7 @@ class DriverDSLImpl(
|
||||
localNetworkMap,
|
||||
spec.rpcUsers,
|
||||
spec.verifierType,
|
||||
customOverrides = notaryConfig(clusterAddress)
|
||||
)
|
||||
customOverrides = notaryConfig(clusterAddress))
|
||||
|
||||
// All other nodes will join the cluster
|
||||
val restNodeFutures = nodeNames.drop(1).map {
|
||||
@ -1034,6 +1051,7 @@ interface InternalDriverDSL : DriverDSL {
|
||||
maximumHeapSize: String = defaultParameters.maximumHeapSize,
|
||||
additionalCordapps: Collection<TestCordapp> = defaultParameters.additionalCordapps,
|
||||
regenerateCordappsOnStart: Boolean = defaultParameters.regenerateCordappsOnStart,
|
||||
flowOverrides: Map<out Class<out FlowLogic<*>>, Class<out FlowLogic<*>>> = emptyMap(),
|
||||
bytemanPort: Int? = null
|
||||
): CordaFuture<NodeHandle>
|
||||
}
|
||||
|
@ -30,6 +30,8 @@ import net.corda.core.utilities.seconds
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.node.internal.AbstractNode
|
||||
import net.corda.node.internal.InitiatedFlowFactory
|
||||
import net.corda.node.internal.NodeFlowManager
|
||||
import net.corda.node.internal.cordapp.JarScanningCordappLoader
|
||||
import net.corda.node.services.api.FlowStarter
|
||||
import net.corda.node.services.api.ServiceHubInternal
|
||||
import net.corda.node.services.api.StartedNodeServices
|
||||
@ -75,7 +77,8 @@ data class MockNodeArgs(
|
||||
val network: InternalMockNetwork,
|
||||
val id: Int,
|
||||
val entropyRoot: BigInteger,
|
||||
val version: VersionInfo = MOCK_VERSION_INFO
|
||||
val version: VersionInfo = MOCK_VERSION_INFO,
|
||||
val flowManager: MockNodeFlowManager = MockNodeFlowManager()
|
||||
)
|
||||
|
||||
// TODO We don't need a parameters object as this is internal only
|
||||
@ -85,7 +88,8 @@ data class InternalMockNodeParameters(
|
||||
val entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()),
|
||||
val configOverrides: (NodeConfiguration) -> Any? = {},
|
||||
val version: VersionInfo = MOCK_VERSION_INFO,
|
||||
val additionalCordapps: Collection<TestCordapp>? = null) {
|
||||
val additionalCordapps: Collection<TestCordapp>? = null,
|
||||
val flowManager: MockNodeFlowManager = MockNodeFlowManager()) {
|
||||
constructor(mockNodeParameters: MockNodeParameters) : this(
|
||||
mockNodeParameters.forcedID,
|
||||
mockNodeParameters.legalName,
|
||||
@ -128,12 +132,10 @@ interface TestStartedNode {
|
||||
* starts up for all [FlowLogic] classes it finds which are annotated with [InitiatedBy].
|
||||
* @return An [Observable] of the initiated flows started by counterparties.
|
||||
*/
|
||||
fun <T : FlowLogic<*>> registerInitiatedFlow(initiatedFlowClass: Class<T>): Observable<T>
|
||||
fun <T : FlowLogic<*>> registerInitiatedFlow(initiatedFlowClass: Class<T>, track: Boolean = false): Observable<T>
|
||||
|
||||
fun <T : FlowLogic<*>> registerInitiatedFlow(initiatingFlowClass: Class<out FlowLogic<*>>, initiatedFlowClass: Class<T>, track: Boolean = false): Observable<T>
|
||||
|
||||
fun <F : FlowLogic<*>> registerFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>,
|
||||
flowFactory: InitiatedFlowFactory<F>,
|
||||
initiatedFlowClass: Class<F>,
|
||||
track: Boolean): Observable<F>
|
||||
}
|
||||
|
||||
open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNetworkParameters(),
|
||||
@ -198,7 +200,8 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
|
||||
*/
|
||||
val defaultNotaryIdentity: Party
|
||||
get() {
|
||||
return defaultNotaryNode.info.legalIdentities.singleOrNull() ?: throw IllegalStateException("Default notary has multiple identities")
|
||||
return defaultNotaryNode.info.legalIdentities.singleOrNull()
|
||||
?: throw IllegalStateException("Default notary has multiple identities")
|
||||
}
|
||||
|
||||
/**
|
||||
@ -266,11 +269,12 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
|
||||
}
|
||||
}
|
||||
|
||||
open class MockNode(args: MockNodeArgs) : AbstractNode<TestStartedNode>(
|
||||
open class MockNode(args: MockNodeArgs, private val mockFlowManager: MockNodeFlowManager = args.flowManager) : AbstractNode<TestStartedNode>(
|
||||
args.config,
|
||||
TestClock(Clock.systemUTC()),
|
||||
EnterpriseNamedCacheFactory(args.config.enterpriseConfiguration.getTracingConfig()),
|
||||
args.version,
|
||||
mockFlowManager,
|
||||
args.network.getServerThread(args.id),
|
||||
args.network.busyLatch
|
||||
) {
|
||||
@ -290,24 +294,28 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
|
||||
override val rpcOps: CordaRPCOps,
|
||||
override val notaryService: NotaryService?) : TestStartedNode {
|
||||
|
||||
override fun <F : FlowLogic<*>> registerFlowFactory(
|
||||
initiatingFlowClass: Class<out FlowLogic<*>>,
|
||||
flowFactory: InitiatedFlowFactory<F>,
|
||||
initiatedFlowClass: Class<F>,
|
||||
track: Boolean): Observable<F> =
|
||||
internals.internalRegisterFlowFactory(smm, initiatingFlowClass, flowFactory, initiatedFlowClass, track)
|
||||
|
||||
override fun dispose() = internals.stop()
|
||||
|
||||
override fun <T : FlowLogic<*>> registerInitiatedFlow(initiatedFlowClass: Class<T>): Observable<T> =
|
||||
internals.registerInitiatedFlow(smm, initiatedFlowClass)
|
||||
override fun <T : FlowLogic<*>> registerInitiatedFlow(initiatedFlowClass: Class<T>, track: Boolean): Observable<T> {
|
||||
internals.flowManager.registerInitiatedFlow(initiatedFlowClass)
|
||||
return smm.changes.filter { it is StateMachineManager.Change.Add }.map { it.logic }.ofType(initiatedFlowClass)
|
||||
}
|
||||
|
||||
override fun <T : FlowLogic<*>> registerInitiatedFlow(initiatingFlowClass: Class<out FlowLogic<*>>, initiatedFlowClass: Class<T>, track: Boolean): Observable<T> {
|
||||
internals.flowManager.registerInitiatedFlow(initiatingFlowClass, initiatedFlowClass)
|
||||
return smm.changes.filter { it is StateMachineManager.Change.Add }.map { it.logic }.ofType(initiatedFlowClass)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
val mockNet = args.network
|
||||
val id = args.id
|
||||
|
||||
init {
|
||||
require(id >= 0) { "Node ID must be zero or positive, was passed: $id" }
|
||||
}
|
||||
|
||||
private val entropyRoot = args.entropyRoot
|
||||
var counter = entropyRoot
|
||||
override val log get() = staticLog
|
||||
@ -329,7 +337,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
|
||||
this,
|
||||
attachments,
|
||||
network as MockNodeMessagingService,
|
||||
object : StartedNodeServices, ServiceHubInternal by services, FlowStarter by flowStarter { },
|
||||
object : StartedNodeServices, ServiceHubInternal by services, FlowStarter by flowStarter {},
|
||||
nodeInfo,
|
||||
smm,
|
||||
database,
|
||||
@ -413,8 +421,19 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
|
||||
var acceptableLiveFiberCountOnStop: Int = 0
|
||||
|
||||
override fun acceptableLiveFiberCountOnStop(): Int = acceptableLiveFiberCountOnStop
|
||||
|
||||
fun <T : FlowLogic<*>> registerInitiatedFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>, initiatedFlowClass: Class<T>, factory: InitiatedFlowFactory<T>, track: Boolean): Observable<T> {
|
||||
mockFlowManager.registerTestingFactory(initiatingFlowClass, factory)
|
||||
return if (track) {
|
||||
smm.changes.filter { it is StateMachineManager.Change.Add }.map { it.logic }.ofType(initiatedFlowClass)
|
||||
} else {
|
||||
Observable.empty<T>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun createUnstartedNode(parameters: InternalMockNodeParameters = InternalMockNodeParameters()): MockNode {
|
||||
return createUnstartedNode(parameters, defaultFactory)
|
||||
}
|
||||
@ -450,7 +469,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
|
||||
val cordappDirectories = cordapps.map { TestCordappDirectories.getJarDirectory(it) }.distinct()
|
||||
doReturn(cordappDirectories).whenever(config).cordappDirectories
|
||||
|
||||
val node = nodeFactory(MockNodeArgs(config, this, id, parameters.entropyRoot, parameters.version))
|
||||
val node = nodeFactory(MockNodeArgs(config, this, id, parameters.entropyRoot, parameters.version, flowManager = parameters.flowManager))
|
||||
_nodes += node
|
||||
if (start) {
|
||||
node.start()
|
||||
@ -479,8 +498,10 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun runNetwork(rounds: Int = -1) {
|
||||
check(!networkSendManuallyPumped) { "MockNetwork.runNetwork() should only be used when networkSendManuallyPumped == false. " +
|
||||
"You can use MockNetwork.waitQuiescent() to wait for all the nodes to process all the messages on their queues instead." }
|
||||
check(!networkSendManuallyPumped) {
|
||||
"MockNetwork.runNetwork() should only be used when networkSendManuallyPumped == false. " +
|
||||
"You can use MockNetwork.waitQuiescent() to wait for all the nodes to process all the messages on their queues instead."
|
||||
}
|
||||
fun pumpAll() = messagingNetwork.endpoints.map { it.pumpReceive(false) }
|
||||
|
||||
if (rounds == -1) {
|
||||
@ -573,3 +594,17 @@ private fun mockNodeConfiguration(certificatesDirectory: Path): NodeConfiguratio
|
||||
)).whenever(it).enterpriseConfiguration
|
||||
}
|
||||
}
|
||||
|
||||
class MockNodeFlowManager : NodeFlowManager() {
|
||||
val testingRegistrations = HashMap<Class<out FlowLogic<*>>, InitiatedFlowFactory<*>>()
|
||||
override fun getFlowFactoryForInitiatingFlow(initiatedFlowClass: Class<out FlowLogic<*>>): InitiatedFlowFactory<*>? {
|
||||
if (initiatedFlowClass in testingRegistrations) {
|
||||
return testingRegistrations.get(initiatedFlowClass)
|
||||
}
|
||||
return super.getFlowFactoryForInitiatingFlow(initiatedFlowClass)
|
||||
}
|
||||
|
||||
fun registerTestingFactory(initiator: Class<out FlowLogic<*>>, factory: InitiatedFlowFactory<*>) {
|
||||
testingRegistrations.put(initiator, factory)
|
||||
}
|
||||
}
|
@ -11,6 +11,8 @@ import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.node.internal.EnterpriseNode
|
||||
import net.corda.node.internal.FlowManager
|
||||
import net.corda.node.internal.NodeFlowManager
|
||||
import net.corda.node.internal.NodeWithInfo
|
||||
import net.corda.node.services.config.*
|
||||
import net.corda.nodeapi.internal.config.toConfig
|
||||
@ -31,6 +33,7 @@ import rx.internal.schedulers.CachedThreadScheduler
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.test.assertFalse
|
||||
|
||||
// TODO Some of the logic here duplicates what's in the driver - the reason why it's not straightforward to replace it by
|
||||
// using DriverDSLImpl in `init()` and `stopAllNodes()` is because of the platform version passed to nodes (driver doesn't
|
||||
@ -89,7 +92,8 @@ abstract class NodeBasedTest(private val cordappPackages: List<String> = emptyLi
|
||||
fun initNode(legalName: CordaX500Name,
|
||||
platformVersion: Int = PLATFORM_VERSION,
|
||||
rpcUsers: List<User> = emptyList(),
|
||||
configOverrides: Map<String, Any> = emptyMap()): InProcessNode {
|
||||
configOverrides: Map<String, Any> = emptyMap(),
|
||||
flowManager: FlowManager = NodeFlowManager(FlowOverrideConfig())): InProcessNode {
|
||||
val baseDirectory = baseDirectory(legalName).createDirectories()
|
||||
val p2pAddress = configOverrides["p2pAddress"] ?: portAllocation.nextHostAndPort().toString()
|
||||
val config = ConfigHelper.loadConfig(
|
||||
@ -122,14 +126,15 @@ abstract class NodeBasedTest(private val cordappPackages: List<String> = emptyLi
|
||||
}
|
||||
|
||||
defaultNetworkParameters.install(baseDirectory)
|
||||
return InProcessNode(parsedConfig, MOCK_VERSION_INFO.copy(platformVersion = platformVersion))
|
||||
return InProcessNode(parsedConfig, MOCK_VERSION_INFO.copy(platformVersion = platformVersion), flowManager)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun startNode(legalName: CordaX500Name,
|
||||
platformVersion: Int = PLATFORM_VERSION,
|
||||
rpcUsers: List<User> = emptyList(),
|
||||
configOverrides: Map<String, Any> = emptyMap()): NodeWithInfo {
|
||||
configOverrides: Map<String, Any> = emptyMap(),
|
||||
flowManager: FlowManager = NodeFlowManager(FlowOverrideConfig())): NodeWithInfo {
|
||||
val node = initNode(legalName,platformVersion, rpcUsers,configOverrides)
|
||||
val nodeInfo = node.start()
|
||||
val nodeWithInfo = NodeWithInfo(node, nodeInfo)
|
||||
@ -142,7 +147,6 @@ abstract class NodeBasedTest(private val cordappPackages: List<String> = emptyLi
|
||||
|
||||
return nodeWithInfo
|
||||
}
|
||||
|
||||
protected fun baseDirectory(legalName: CordaX500Name): Path {
|
||||
return tempFolder.root.toPath() / legalName.organisation.replace(WHITESPACE, "")
|
||||
}
|
||||
@ -157,10 +161,9 @@ abstract class NodeBasedTest(private val cordappPackages: List<String> = emptyLi
|
||||
}
|
||||
}
|
||||
|
||||
class InProcessNode(configuration: NodeConfiguration, versionInfo: VersionInfo) : EnterpriseNode(configuration, versionInfo, false) {
|
||||
|
||||
class InProcessNode(configuration: NodeConfiguration, versionInfo: VersionInfo, flowManager: FlowManager = NodeFlowManager(configuration.flowOverrides)) : EnterpriseNode(configuration, versionInfo, false, flowManager = flowManager) {
|
||||
override fun start() : NodeInfo {
|
||||
check(isValidJavaVersion()) { "You are using a version of Java that is not supported (${SystemUtils.JAVA_VERSION}). Please upgrade to the latest version of Java 8." }
|
||||
assertFalse(isInvalidJavaVersion(), "You are using a version of Java that is not supported (${SystemUtils.JAVA_VERSION}). Please upgrade to the latest version of Java 8." )
|
||||
return super.start()
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
package net.corda.testing.node.internal
|
||||
|
||||
import com.typesafe.config.ConfigValueFactory
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.internal.deleteRecursively
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.writeText
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.testing.node.TestCordapp
|
||||
@ -22,24 +24,28 @@ object TestCordappDirectories {
|
||||
fun getJarDirectory(cordapp: TestCordapp, cordappsDirectory: Path = defaultCordappsDirectory): Path {
|
||||
cordapp as TestCordappImpl
|
||||
return testCordappsCache.computeIfAbsent(cordapp) {
|
||||
val cordappDir = (cordappsDirectory / UUID.randomUUID().toString()).createDirectories()
|
||||
val uniqueScanString = if (cordapp.packages.size == 1 && cordapp.classes.isEmpty()) {
|
||||
cordapp.packages.first()
|
||||
} else {
|
||||
"${cordapp.packages}${cordapp.classes.joinToString { it.name }}".toByteArray().sha256().toString()
|
||||
val configString = ConfigValueFactory.fromMap(cordapp.config).toConfig().root().render()
|
||||
val filename = cordapp.run {
|
||||
val uniqueScanString = if (packages.size == 1 && classes.isEmpty() && config.isEmpty()) {
|
||||
packages.first()
|
||||
} else {
|
||||
"$packages$classes$configString".toByteArray().sha256().toString()
|
||||
}
|
||||
"${name}_${vendor}_${title}_${version}_${targetVersion}_$uniqueScanString".replace(whitespace, "-")
|
||||
}
|
||||
val jarFileName = cordapp.run { "${name}_${vendor}_${title}_${version}_${targetVersion}_$uniqueScanString.jar".replace(whitespace, "-") }
|
||||
val jarFile = cordappDir / jarFileName
|
||||
val cordappDir = cordappsDirectory / UUID.randomUUID().toString()
|
||||
val configDir = (cordappDir / "config").createDirectories()
|
||||
val jarFile = cordappDir / "$filename.jar"
|
||||
cordapp.packageAsJar(jarFile)
|
||||
(configDir / "$filename.conf").writeText(configString)
|
||||
logger.debug { "$cordapp packaged into $jarFile" }
|
||||
cordappDir
|
||||
}
|
||||
}
|
||||
|
||||
private val defaultCordappsDirectory: Path by lazy {
|
||||
val cordappsDirectory = (Paths.get("build") / "tmp" / getTimestampAsDirectoryName() / "generated-test-cordapps").toAbsolutePath()
|
||||
val cordappsDirectory = Paths.get("build").toAbsolutePath() / "generated-test-cordapps" / getTimestampAsDirectoryName()
|
||||
logger.info("Initialising generated test CorDapps directory in $cordappsDirectory")
|
||||
cordappsDirectory.toFile().deleteOnExit()
|
||||
cordappsDirectory.deleteRecursively()
|
||||
cordappsDirectory.createDirectories()
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ data class TestCordappImpl(override val name: String,
|
||||
override val vendor: String,
|
||||
override val title: String,
|
||||
override val targetVersion: Int,
|
||||
override val config: Map<String, Any>,
|
||||
override val packages: Set<String>,
|
||||
val classes: Set<Class<*>>) : TestCordapp {
|
||||
|
||||
@ -20,6 +21,8 @@ data class TestCordappImpl(override val name: String,
|
||||
|
||||
override fun withTargetVersion(targetVersion: Int): TestCordappImpl = copy(targetVersion = targetVersion)
|
||||
|
||||
override fun withConfig(config: Map<String, Any>): TestCordappImpl = copy(config = config)
|
||||
|
||||
fun withClasses(vararg classes: Class<*>): TestCordappImpl {
|
||||
return copy(classes = classes.filter { clazz -> packages.none { clazz.name.startsWith("$it.") } }.toSet())
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ apply plugin: 'java'
|
||||
apply plugin: 'kotlin'
|
||||
|
||||
dependencies {
|
||||
compile group: 'info.picocli', name: 'picocli', version: '3.0.1'
|
||||
compile "info.picocli:picocli:$picocli_version"
|
||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
compile group: "com.fasterxml.jackson.dataformat", name: "jackson-dataformat-yaml", version: "2.9.0"
|
||||
compile group: "com.fasterxml.jackson.core", name: "jackson-databind", version: "2.9.0"
|
||||
|
@ -3,7 +3,6 @@ package net.corda.testing.internal
|
||||
import net.corda.core.contracts.ContractClassName
|
||||
import net.corda.core.cordapp.Cordapp
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER
|
||||
import net.corda.core.internal.cordapp.CordappImpl
|
||||
import net.corda.core.node.services.AttachmentId
|
||||
@ -13,7 +12,6 @@ import net.corda.node.internal.cordapp.CordappProviderImpl
|
||||
import net.corda.testing.services.MockAttachmentStorage
|
||||
import java.nio.file.Paths
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
class MockCordappProvider(
|
||||
cordappLoader: CordappLoader,
|
||||
|
@ -14,7 +14,6 @@ dependencies {
|
||||
exclude module: 'node-api'
|
||||
exclude module: 'finance'
|
||||
}
|
||||
testCompile project(':test-utils')
|
||||
}
|
||||
|
||||
jar {
|
||||
|
@ -47,7 +47,7 @@ class BlobInspector : CordaCliWrapper("blob-inspector", "Convert AMQP serialised
|
||||
description = ["Display the owningKey and certPath properties of Party and PartyAndReference objects respectively"])
|
||||
private var fullParties: Boolean = false
|
||||
|
||||
@Option(names = ["--schema"], description = ["Print the blob's schema first"])
|
||||
@Option(names = ["--schema"], description = ["Prints the blob's schema first"])
|
||||
private var schema: Boolean = false
|
||||
|
||||
override fun runProgram() = run(System.out)
|
||||
|
@ -0,0 +1,58 @@
|
||||
- commandName: "<main class>"
|
||||
positionalParams:
|
||||
- parameterName: "0"
|
||||
parameterType: "java.net.URL"
|
||||
required: true
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
params:
|
||||
- parameterName: "--format"
|
||||
parameterType: "net.corda.blobinspector.OutputFormatType"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues:
|
||||
- "YAML"
|
||||
- "JSON"
|
||||
- parameterName: "--full-parties"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "--input-format"
|
||||
parameterType: "net.corda.blobinspector.InputFormatType"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues:
|
||||
- "BINARY"
|
||||
- "HEX"
|
||||
- "BASE64"
|
||||
- parameterName: "--log-to-console"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "--logging-level"
|
||||
parameterType: "org.slf4j.event.Level"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues:
|
||||
- "ERROR"
|
||||
- "WARN"
|
||||
- "INFO"
|
||||
- "DEBUG"
|
||||
- "TRACE"
|
||||
- parameterName: "--schema"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "--verbose"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "-v"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
@ -25,7 +25,7 @@ class NetworkBootstrapperRunner : CordaCliWrapper("bootstrapper", "Bootstrap a l
|
||||
@Option(names = ["--no-copy"], description = ["""Don't copy the CorDapp JARs into the nodes' "cordapps" directories."""])
|
||||
private var noCopy: Boolean = false
|
||||
|
||||
@Option(names = ["--minimum-platform-version"], description = ["The minimumPlatformVersion to use in the network-parameters"])
|
||||
@Option(names = ["--minimum-platform-version"], description = ["The minimumPlatformVersion to use in the network-parameters."])
|
||||
private var minimumPlatformVersion = PLATFORM_VERSION
|
||||
|
||||
override fun runProgram(): Int {
|
||||
|
@ -6,11 +6,6 @@
|
||||
required: false
|
||||
multiParam: true
|
||||
acceptableValues: []
|
||||
- parameterName: "--install-shell-extensions"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "--log-to-console"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
|
@ -69,57 +69,43 @@ fun CordaCliWrapper.start(args: Array<String>) {
|
||||
cmd.registerConverter(Path::class.java) { Paths.get(it).toAbsolutePath().normalize() }
|
||||
cmd.commandSpec.name(alias)
|
||||
cmd.commandSpec.usageMessage().description(description)
|
||||
cmd.commandSpec.parser().collectErrors(true)
|
||||
this.subCommands().forEach {
|
||||
val subCommand = CommandLine(it)
|
||||
it.args = args
|
||||
subCommand.commandSpec.usageMessage().description(it.description)
|
||||
cmd.commandSpec.addSubcommand(it.alias, subCommand)
|
||||
}
|
||||
|
||||
try {
|
||||
val defaultAnsiMode = if (CordaSystemUtils.isOsWindows()) {
|
||||
Help.Ansi.ON
|
||||
} else {
|
||||
Help.Ansi.AUTO
|
||||
}
|
||||
|
||||
val results = cmd.parse(*args)
|
||||
val app = cmd.getCommand<CordaCliWrapper>()
|
||||
if (cmd.isUsageHelpRequested) {
|
||||
cmd.usage(System.out, defaultAnsiMode)
|
||||
exitProcess(ExitCodes.SUCCESS)
|
||||
}
|
||||
if (cmd.isVersionHelpRequested) {
|
||||
cmd.printVersionHelp(System.out, defaultAnsiMode)
|
||||
exitProcess(ExitCodes.SUCCESS)
|
||||
}
|
||||
if (app.installShellExtensionsParser.installShellExtensions) {
|
||||
System.out.println("Install shell extensions: ${app.installShellExtensionsParser.installShellExtensions}")
|
||||
// ignore any parsing errors and run the program
|
||||
exitProcess(app.call())
|
||||
}
|
||||
val allErrors = results.flatMap { it.parseResult?.errors() ?: emptyList() }
|
||||
if (allErrors.any()) {
|
||||
val parameterExceptions = allErrors.asSequence().filter { it is ParameterException }
|
||||
if (parameterExceptions.any()) {
|
||||
System.err.println("${ShellConstants.RED}${parameterExceptions.map{ it.message }.joinToString()}${ShellConstants.RESET}")
|
||||
parameterExceptions.filter { it is UnmatchedArgumentException}.forEach { (it as UnmatchedArgumentException).printSuggestions(System.out) }
|
||||
usage(cmd, System.out, defaultAnsiMode)
|
||||
val results = cmd.parseWithHandlers(RunLast().useOut(System.out).useAnsi(defaultAnsiMode),
|
||||
DefaultExceptionHandler<List<Any>>().useErr(System.err).useAnsi(defaultAnsiMode),
|
||||
*args)
|
||||
// If an error code has been returned, use this and exit
|
||||
results?.firstOrNull()?.let {
|
||||
if (it is Int) {
|
||||
exitProcess(it)
|
||||
} else {
|
||||
exitProcess(ExitCodes.FAILURE)
|
||||
}
|
||||
throw allErrors.first()
|
||||
}
|
||||
exitProcess(app.call())
|
||||
} catch (e: Exception) {
|
||||
// If no results returned, picocli ran something without invoking the main program, e.g. --help or --version, so exit successfully
|
||||
exitProcess(ExitCodes.SUCCESS)
|
||||
} catch (e: ExecutionException) {
|
||||
val throwable = e.cause ?: e
|
||||
if (this.verbose) {
|
||||
throwable.printStackTrace()
|
||||
} else {
|
||||
System.err.println("${ShellConstants.RED}${throwable.rootMessage ?: "Use --verbose for more details"}${ShellConstants.RESET}")
|
||||
System.err.println("*ERROR*: ${throwable.rootMessage ?: "Use --verbose for more details"}")
|
||||
}
|
||||
exitProcess(ExitCodes.FAILURE)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple base class for handling help, version, verbose and logging-level commands.
|
||||
* As versionProvider information from the MANIFEST file is used. It can be overwritten by custom version providers (see: Node)
|
||||
* Picocli will prioritise versionProvider from the `@Command` annotation on the subclass, see: https://picocli.info/#_reuse_combinations
|
||||
*/
|
||||
@Command(mixinStandardHelpOptions = true,
|
||||
versionProvider = CordaVersionProvider::class,
|
||||
sortOptions = false,
|
||||
@ -129,9 +115,9 @@ fun CordaCliWrapper.start(args: Array<String>) {
|
||||
parameterListHeading = "%n@|bold,underline Parameters|@:%n%n",
|
||||
optionListHeading = "%n@|bold,underline Options|@:%n%n",
|
||||
commandListHeading = "%n@|bold,underline Commands|@:%n%n")
|
||||
abstract class CordaCliWrapper(val alias: String, val description: String) : Callable<Int> {
|
||||
abstract class CliWrapperBase(val alias: String, val description: String) : Callable<Int> {
|
||||
companion object {
|
||||
private val logger by lazy { loggerFor<CordaCliWrapper>() }
|
||||
private val logger by lazy { contextLogger() }
|
||||
}
|
||||
|
||||
// Raw args are provided for use in logging - this is a lateinit var rather than a constructor parameter as the class
|
||||
@ -148,9 +134,6 @@ abstract class CordaCliWrapper(val alias: String, val description: String) : Cal
|
||||
)
|
||||
var loggingLevel: Level = Level.INFO
|
||||
|
||||
@Mixin
|
||||
lateinit var installShellExtensionsParser: InstallShellExtensionsParser
|
||||
|
||||
// This needs to be called before loggers (See: NodeStartup.kt:51 logger called by lazy, initLogging happens before).
|
||||
// Node's logging is more rich. In corda configurations two properties, defaultLoggingLevel and consoleLogLevel, are usually used.
|
||||
open fun initLogging() {
|
||||
@ -169,7 +152,32 @@ abstract class CordaCliWrapper(val alias: String, val description: String) : Cal
|
||||
override fun call(): Int {
|
||||
initLogging()
|
||||
logger.info("Application Args: ${args.joinToString(" ")}")
|
||||
installShellExtensionsParser.installOrUpdateShellExtensions(alias, this.javaClass.name)
|
||||
return runProgram()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple base class for handling help, version, verbose and logging-level commands.
|
||||
* As versionProvider information from the MANIFEST file is used. It can be overwritten by custom version providers (see: Node)
|
||||
* Picocli will prioritise versionProvider from the `@Command` annotation on the subclass, see: https://picocli.info/#_reuse_combinations
|
||||
*/
|
||||
abstract class CordaCliWrapper(alias: String, description: String) : CliWrapperBase(alias, description) {
|
||||
companion object {
|
||||
private val logger by lazy { contextLogger() }
|
||||
}
|
||||
|
||||
private val installShellExtensionsParser = InstallShellExtensionsParser(this)
|
||||
|
||||
protected open fun additionalSubCommands(): Set<CliWrapperBase> = emptySet()
|
||||
|
||||
fun subCommands(): Set<CliWrapperBase> {
|
||||
return additionalSubCommands() + installShellExtensionsParser
|
||||
}
|
||||
|
||||
override fun call(): Int {
|
||||
initLogging()
|
||||
logger.info("Application Args: ${args.joinToString(" ")}")
|
||||
installShellExtensionsParser.updateShellExtensions()
|
||||
return runProgram()
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,13 @@ package net.corda.cliutils
|
||||
|
||||
import net.corda.core.internal.*
|
||||
import picocli.CommandLine
|
||||
import picocli.CommandLine.Command
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.util.*
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
private class ShellExtensionsGenerator(val alias: String, val className: String) {
|
||||
private class ShellExtensionsGenerator(val parent: CordaCliWrapper) {
|
||||
private class SettingsFile(val filePath: Path) {
|
||||
private val lines: MutableList<String> by lazy { getFileLines() }
|
||||
var fileModified: Boolean = false
|
||||
@ -68,25 +68,27 @@ private class ShellExtensionsGenerator(val alias: String, val className: String)
|
||||
private fun jarVersion(alias: String) = "# $alias - Version: ${CordaVersionProvider.releaseVersion}, Revision: ${CordaVersionProvider.revision}"
|
||||
private fun getAutoCompleteFileLocation(alias: String) = userHome / ".completion" / alias
|
||||
|
||||
private fun generateAutoCompleteFile(alias: String, className: String) {
|
||||
private fun generateAutoCompleteFile(alias: String) {
|
||||
println("Generating $alias auto completion file")
|
||||
val autoCompleteFile = getAutoCompleteFileLocation(alias)
|
||||
autoCompleteFile.parent.createDirectories()
|
||||
picocli.AutoComplete.main("-f", "-n", alias, className, "-o", autoCompleteFile.toStringWithDeWindowsfication())
|
||||
val hierarchy = CommandLine(parent)
|
||||
parent.subCommands().forEach { hierarchy.addSubcommand(it.alias, it)}
|
||||
|
||||
// Append hash of file to autocomplete file
|
||||
autoCompleteFile.toFile().appendText(jarVersion(alias))
|
||||
val builder = StringBuilder(picocli.AutoComplete.bash(alias, hierarchy))
|
||||
builder.append(jarVersion(alias))
|
||||
autoCompleteFile.writeText(builder.toString())
|
||||
}
|
||||
|
||||
fun installShellExtensions() {
|
||||
// Get jar location and generate alias command
|
||||
val command = "alias $alias='java -jar \"${jarLocation.toStringWithDeWindowsfication()}\"'"
|
||||
generateAutoCompleteFile(alias, className)
|
||||
val command = "alias ${parent.alias}='java -jar \"${jarLocation.toStringWithDeWindowsfication()}\"'"
|
||||
generateAutoCompleteFile(parent.alias)
|
||||
|
||||
// Get bash settings file
|
||||
val bashSettingsFile = SettingsFile(userHome / ".bashrc")
|
||||
// Replace any existing alias. There can be only one.
|
||||
bashSettingsFile.addOrReplaceIfStartsWith("alias $alias", command)
|
||||
bashSettingsFile.addOrReplaceIfStartsWith("alias ${parent.alias}", command)
|
||||
val completionFileCommand = "for bcfile in ~/.completion/* ; do . \$bcfile; done"
|
||||
bashSettingsFile.addIfNotExists(completionFileCommand)
|
||||
bashSettingsFile.updateAndBackupIfNecessary()
|
||||
@ -95,17 +97,17 @@ private class ShellExtensionsGenerator(val alias: String, val className: String)
|
||||
val zshSettingsFile = SettingsFile(userHome / ".zshrc")
|
||||
zshSettingsFile.addIfNotExists("autoload -U +X compinit && compinit")
|
||||
zshSettingsFile.addIfNotExists("autoload -U +X bashcompinit && bashcompinit")
|
||||
zshSettingsFile.addOrReplaceIfStartsWith("alias $alias", command)
|
||||
zshSettingsFile.addOrReplaceIfStartsWith("alias ${parent.alias}", command)
|
||||
zshSettingsFile.addIfNotExists(completionFileCommand)
|
||||
zshSettingsFile.updateAndBackupIfNecessary()
|
||||
|
||||
println("Installation complete, $alias is available in bash with autocompletion. ")
|
||||
println("Type `$alias <options>` from the commandline.")
|
||||
println("Installation complete, ${parent.alias} is available in bash with autocompletion. ")
|
||||
println("Type `${parent.alias} <options>` from the commandline.")
|
||||
println("Restart bash for this to take effect, or run `. ~/.bashrc` in bash or `. ~/.zshrc` in zsh to re-initialise your shell now")
|
||||
}
|
||||
|
||||
fun checkForAutoCompleteUpdate() {
|
||||
val autoCompleteFile = getAutoCompleteFileLocation(alias)
|
||||
val autoCompleteFile = getAutoCompleteFileLocation(parent.alias)
|
||||
|
||||
// If no autocomplete file, it hasn't been installed, so don't do anything
|
||||
if (!autoCompleteFile.exists()) return
|
||||
@ -113,25 +115,21 @@ private class ShellExtensionsGenerator(val alias: String, val className: String)
|
||||
var lastLine = ""
|
||||
autoCompleteFile.toFile().forEachLine { lastLine = it }
|
||||
|
||||
if (lastLine != jarVersion(alias)) {
|
||||
if (lastLine != jarVersion(parent.alias)) {
|
||||
println("Old auto completion file detected... regenerating")
|
||||
generateAutoCompleteFile(alias, className)
|
||||
generateAutoCompleteFile(parent.alias)
|
||||
println("Restart bash for this to take effect, or run `. ~/.bashrc` to re-initialise bash now")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class InstallShellExtensionsParser {
|
||||
@CommandLine.Option(names = ["--install-shell-extensions"], description = ["Install alias and autocompletion for bash and zsh"])
|
||||
var installShellExtensions: Boolean = false
|
||||
|
||||
fun installOrUpdateShellExtensions(alias: String, className: String) {
|
||||
val generator = ShellExtensionsGenerator(alias, className)
|
||||
if (installShellExtensions) {
|
||||
generator.installShellExtensions()
|
||||
exitProcess(0)
|
||||
} else {
|
||||
generator.checkForAutoCompleteUpdate()
|
||||
}
|
||||
@Command(helpCommand = true)
|
||||
class InstallShellExtensionsParser(private val cliWrapper: CordaCliWrapper) : CliWrapperBase("install-shell-extensions", "Install alias and autocompletion for bash and zsh") {
|
||||
private val generator = ShellExtensionsGenerator(cliWrapper)
|
||||
override fun runProgram(): Int {
|
||||
generator.installShellExtensions()
|
||||
return ExitCodes.SUCCESS
|
||||
}
|
||||
|
||||
fun updateShellExtensions() = generator.checkForAutoCompleteUpdate()
|
||||
}
|
@ -85,7 +85,7 @@ class Explorer internal constructor(private val explorerController: ExplorerCont
|
||||
// Note: does not copy dependencies because we should soon be making all apps fat jars and dependencies implicit.
|
||||
//
|
||||
// TODO: Remove this code when serialisation has been upgraded.
|
||||
val cordappsDir = config.explorerDir / NodeConfig.cordappDirName
|
||||
val cordappsDir = config.explorerDir / NodeConfig.CORDAPP_DIR_NAME
|
||||
cordappsDir.createDirectories()
|
||||
config.cordappsDir.list {
|
||||
it.forEachOrdered { path ->
|
||||
|
@ -3,6 +3,7 @@ package net.corda.demobench.model
|
||||
import com.typesafe.config.*
|
||||
import com.typesafe.config.ConfigFactory.empty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.internal.copyToDirectory
|
||||
import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.internal.div
|
||||
@ -11,7 +12,7 @@ import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.nodeapi.internal.config.toConfig
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.util.Properties
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* This is a subset of FullNodeConfiguration, containing only those configs which we need. The node uses reference.conf
|
||||
@ -38,33 +39,33 @@ data class NodeConfig(
|
||||
companion object {
|
||||
val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false)
|
||||
val defaultUser = user("guest")
|
||||
const val cordappDirName = "cordapps"
|
||||
const val CORDAPP_DIR_NAME = "cordapps"
|
||||
}
|
||||
|
||||
fun nodeConf(): Config {
|
||||
@VisibleForTesting
|
||||
internal fun nodeConf(): Config {
|
||||
val rpcSettings: ConfigObject = empty()
|
||||
.withValue("address", valueFor(rpcSettings.address.toString()))
|
||||
.withValue("adminAddress", valueFor(rpcSettings.adminAddress.toString()))
|
||||
.root()
|
||||
val customMap: Map<String, Any> = HashMap<String, Any>().also {
|
||||
if (issuableCurrencies.isNotEmpty()) {
|
||||
it["issuableCurrencies"] = issuableCurrencies
|
||||
}
|
||||
}
|
||||
val custom: ConfigObject = ConfigFactory.parseMap(customMap).root()
|
||||
return NodeConfigurationData(myLegalName, p2pAddress, this.rpcSettings.address, notary, h2port, rpcUsers, useTestClock, detectPublicIp, devMode)
|
||||
.toConfig()
|
||||
.withoutPath("rpcAddress")
|
||||
.withoutPath("rpcAdminAddress")
|
||||
.withValue("rpcSettings", rpcSettings)
|
||||
.withOptionalValue("custom", custom)
|
||||
}
|
||||
|
||||
fun webServerConf() = WebServerConfigurationData(myLegalName, rpcSettings.address, webAddress, rpcUsers).asConfig()
|
||||
@VisibleForTesting
|
||||
internal fun webServerConf() = WebServerConfigurationData(myLegalName, rpcSettings.address, webAddress, rpcUsers).asConfig()
|
||||
|
||||
fun toNodeConfText() = nodeConf().render()
|
||||
fun toNodeConfText(): String = nodeConf().render()
|
||||
|
||||
fun toWebServerConfText() = webServerConf().render()
|
||||
fun toWebServerConfText(): String = webServerConf().render()
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun financeConf() = FinanceConfData(issuableCurrencies).toConfig()
|
||||
|
||||
fun toFinanceConfText(): String = financeConf().render()
|
||||
|
||||
fun serialiseAsString(): String = toConfig().render()
|
||||
|
||||
@ -92,6 +93,8 @@ private data class WebServerConfigurationData(
|
||||
fun asConfig() = toConfig()
|
||||
}
|
||||
|
||||
private data class FinanceConfData(val issuableCurrencies: List<String>)
|
||||
|
||||
/**
|
||||
* This is a subset of NotaryConfig. It implements [ExtraService] to avoid unnecessary copying.
|
||||
*/
|
||||
@ -104,7 +107,7 @@ data class NodeConfigWrapper(val baseDir: Path, val nodeConfig: NodeConfig) : Ha
|
||||
val key: String = nodeConfig.myLegalName.organisation.toKey()
|
||||
val nodeDir: Path = baseDir / key
|
||||
val explorerDir: Path = baseDir / "$key-explorer"
|
||||
override val cordappsDir: Path = nodeDir / NodeConfig.cordappDirName
|
||||
override val cordappsDir: Path = nodeDir / NodeConfig.CORDAPP_DIR_NAME
|
||||
var state: NodeState = NodeState.STARTING
|
||||
|
||||
fun install(cordapps: Collection<Path>) {
|
||||
|
@ -112,18 +112,14 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() {
|
||||
try {
|
||||
// Notary can be removed and then added again, that's why we need to perform this check.
|
||||
require((config.nodeConfig.notary != null).xor(notaryIdentity != null)) { "There must be exactly one notary in the network" }
|
||||
config.nodeDir.createDirectories()
|
||||
val cordappConfigDir = (config.cordappsDir / "config").createDirectories()
|
||||
|
||||
// Install any built-in plugins into the working directory.
|
||||
cordappController.populate(config)
|
||||
|
||||
// Write this node's configuration file into its working directory.
|
||||
val confFile = config.nodeDir / "node.conf"
|
||||
confFile.writeText(config.nodeConfig.toNodeConfText())
|
||||
|
||||
// Write this node's configuration file into its working directory.
|
||||
val webConfFile = config.nodeDir / "web-server.conf"
|
||||
webConfFile.writeText(config.nodeConfig.toWebServerConfText())
|
||||
(config.nodeDir / "node.conf").writeText(config.nodeConfig.toNodeConfText())
|
||||
(config.nodeDir / "web-server.conf").writeText(config.nodeConfig.toWebServerConfText())
|
||||
(cordappConfigDir / "${CordappController.FINANCE_CORDAPP_FILENAME}.conf").writeText(config.nodeConfig.toFinanceConfText())
|
||||
|
||||
// Execute the Corda node
|
||||
val cordaEnv = System.getenv().toMutableMap().apply {
|
||||
|
@ -12,10 +12,13 @@ import java.nio.file.StandardCopyOption
|
||||
import kotlin.streams.toList
|
||||
|
||||
class CordappController : Controller() {
|
||||
companion object {
|
||||
const val FINANCE_CORDAPP_FILENAME = "corda-finance"
|
||||
}
|
||||
|
||||
private val jvm by inject<JVMConfig>()
|
||||
private val cordappDir: Path = jvm.applicationDir.resolve(NodeConfig.cordappDirName)
|
||||
private val finance: Path = cordappDir.resolve("corda-finance.jar")
|
||||
private val cordappDir: Path = jvm.applicationDir / NodeConfig.CORDAPP_DIR_NAME
|
||||
private val financeCordappJar: Path = cordappDir / "$FINANCE_CORDAPP_FILENAME.jar"
|
||||
|
||||
/**
|
||||
* Install any built-in cordapps that this node requires.
|
||||
@ -25,8 +28,8 @@ class CordappController : Controller() {
|
||||
if (!config.cordappsDir.exists()) {
|
||||
config.cordappsDir.createDirectories()
|
||||
}
|
||||
if (finance.exists()) {
|
||||
finance.copyToDirectory(config.cordappsDir, StandardCopyOption.REPLACE_EXISTING)
|
||||
if (financeCordappJar.exists()) {
|
||||
financeCordappJar.copyToDirectory(config.cordappsDir, StandardCopyOption.REPLACE_EXISTING)
|
||||
log.info("Installed 'Finance' cordapp")
|
||||
}
|
||||
}
|
||||
@ -39,7 +42,7 @@ class CordappController : Controller() {
|
||||
if (!config.cordappsDir.isDirectory()) return emptyList()
|
||||
return config.cordappsDir.walk(1) { paths ->
|
||||
paths.filter(Path::isCordapp)
|
||||
.filter { !finance.endsWith(it.fileName) }
|
||||
.filter { !financeCordappJar.endsWith(it.fileName) }
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ class ProfileController : Controller() {
|
||||
log.info("Wrote: $file")
|
||||
|
||||
// Write all of the non-built-in cordapps.
|
||||
val cordappDir = (nodeDir / NodeConfig.cordappDirName).createDirectory()
|
||||
val cordappDir = (nodeDir / NodeConfig.CORDAPP_DIR_NAME).createDirectory()
|
||||
cordappController.useCordappsFor(config).forEach {
|
||||
val cordapp = it.copyToDirectory(cordappDir)
|
||||
log.info("Wrote: $cordapp")
|
||||
|
@ -66,12 +66,7 @@ class NodeConfigTest {
|
||||
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"))
|
||||
assertEquals(listOf("GBP"), config.financeConf().getStringList("issuableCurrencies"))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -22,6 +22,7 @@ import net.corda.finance.flows.*
|
||||
import net.corda.finance.flows.CashExitFlow.ExitRequest
|
||||
import net.corda.finance.flows.CashIssueAndPaymentFlow.IssueAndPaymentRequest
|
||||
import net.corda.finance.schemas.CashSchemaV1
|
||||
import net.corda.finance.internal.CashConfigDataFlow
|
||||
import net.corda.node.services.Permissions.Companion.startFlow
|
||||
import net.corda.sample.businessnetwork.iou.IOUFlow
|
||||
import net.corda.sample.businessnetwork.membership.flow.ObtainMembershipListContentFlow
|
||||
@ -29,6 +30,7 @@ import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.driver.*
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.node.internal.FINANCE_CORDAPP
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import kotlin.reflect.KClass
|
||||
@ -75,6 +77,7 @@ class ExplorerSimulation(private val options: OptionSet) {
|
||||
portAllocation = portAllocation,
|
||||
extraCordappPackagesToScan = packagesOfClasses(CashIssueFlow::class, Cash::class, CashSchemaV1::class,
|
||||
IOUFlow::class, ObtainMembershipListContentFlow::class),
|
||||
cordappsForAllNodes = listOf(FINANCE_CORDAPP),
|
||||
waitForAllNodesToFinish = true,
|
||||
jmxPolicy = JmxPolicy(true)
|
||||
)) {
|
||||
@ -83,10 +86,16 @@ class ExplorerSimulation(private val options: OptionSet) {
|
||||
val bob = startNode(providedName = BOB_NAME, rpcUsers = listOf(user))
|
||||
val ukBankName = CordaX500Name(organisation = "UK Bank Plc", locality = "London", country = "GB")
|
||||
val usaBankName = CordaX500Name(organisation = "USA Bank Corp", locality = "New York", country = "US")
|
||||
val issuerGBP = startNode(providedName = ukBankName, rpcUsers = listOf(manager),
|
||||
customOverrides = mapOf("custom" to mapOf("issuableCurrencies" to listOf("GBP"))))
|
||||
val issuerUSD = startNode(providedName = usaBankName, rpcUsers = listOf(manager),
|
||||
customOverrides = mapOf("custom" to mapOf("issuableCurrencies" to listOf("USD"))))
|
||||
val issuerGBP = startNode(
|
||||
providedName = ukBankName,
|
||||
rpcUsers = listOf(manager),
|
||||
additionalCordapps = listOf(FINANCE_CORDAPP.withConfig(mapOf("issuableCurrencies" to listOf("GBP"))))
|
||||
)
|
||||
val issuerUSD = startNode(
|
||||
providedName = usaBankName,
|
||||
rpcUsers = listOf(manager),
|
||||
additionalCordapps = listOf(FINANCE_CORDAPP.withConfig(mapOf("issuableCurrencies" to listOf("USD"))))
|
||||
)
|
||||
val bno = startNode(providedName = IOUFlow.allowedMembershipName, rpcUsers = listOf(user))
|
||||
|
||||
notaryNode = defaultNotaryNode.get()
|
||||
|
@ -7,7 +7,7 @@ import net.corda.client.jfx.utils.ChosenList
|
||||
import net.corda.client.jfx.utils.map
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.flows.CashConfigDataFlow
|
||||
import net.corda.finance.internal.CashConfigDataFlow
|
||||
import tornadofx.*
|
||||
import java.util.*
|
||||
|
||||
|
@ -25,7 +25,7 @@ class NotaryCopier(val cacheDir: File) : NodeCopier(cacheDir) {
|
||||
|
||||
fun generateNodeInfo(dirToGenerateFrom: File): File {
|
||||
val nodeInfoGeneratorProcess = ProcessBuilder()
|
||||
.command(listOf("java", "-jar", "corda.jar", "--just-generate-node-info"))
|
||||
.command(listOf("java", "-jar", "corda.jar", "generate-node-info"))
|
||||
.directory(dirToGenerateFrom)
|
||||
.inheritIO()
|
||||
.start()
|
||||
|
@ -21,11 +21,6 @@
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "--install-shell-extensions"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "--log-to-console"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
|
Loading…
x
Reference in New Issue
Block a user