mirror of
https://github.com/corda/corda.git
synced 2024-12-24 15:16:45 +00:00
ENT-6378 Migrate corda-shell
to external repo (#7005)
Remove the shell code from the OS code base, this includes the modules: - `:tools:shell` - `:tools:shell-cli` The shell will be run within a node if it exists within the node's `drivers` directory. This is done by using a `URLClassloader` to load the `InteractiveShell` class into Corda's JVM process and running `startShell` and `runLocalShell`. Running the shell within the `:samples` will require adding: ``` cordaDriver "net.corda:corda-shell:<corda_shell_version>" ``` To the module's `build.gradle` containing `deployNodes`. The script will then include the shell in the created nodes.
This commit is contained in:
parent
78aed771b2
commit
56c9d6404f
@ -105,7 +105,6 @@ buildscript {
|
|||||||
ext.dependency_checker_version = '5.2.0'
|
ext.dependency_checker_version = '5.2.0'
|
||||||
ext.commons_collections_version = '4.3'
|
ext.commons_collections_version = '4.3'
|
||||||
ext.beanutils_version = '1.9.4'
|
ext.beanutils_version = '1.9.4'
|
||||||
ext.crash_version = '1.7.5'
|
|
||||||
ext.jsr305_version = constants.getProperty("jsr305Version")
|
ext.jsr305_version = constants.getProperty("jsr305Version")
|
||||||
ext.shiro_version = '1.4.1'
|
ext.shiro_version = '1.4.1'
|
||||||
ext.artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion')
|
ext.artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion')
|
||||||
|
@ -94,7 +94,7 @@ processTestResources {
|
|||||||
dependencies {
|
dependencies {
|
||||||
compile project(':node-api')
|
compile project(':node-api')
|
||||||
compile project(':client:rpc')
|
compile project(':client:rpc')
|
||||||
compile project(':tools:shell')
|
compile project(':client:jackson')
|
||||||
compile project(':tools:cliutils')
|
compile project(':tools:cliutils')
|
||||||
compile project(':common-validation')
|
compile project(':common-validation')
|
||||||
compile project(':common-configuration-parsing')
|
compile project(':common-configuration-parsing')
|
||||||
|
@ -83,6 +83,7 @@ import net.corda.node.internal.cordapp.JarScanningCordappLoader
|
|||||||
import net.corda.node.internal.cordapp.VirtualCordapp
|
import net.corda.node.internal.cordapp.VirtualCordapp
|
||||||
import net.corda.node.internal.rpc.proxies.AuthenticatedRpcOpsProxy
|
import net.corda.node.internal.rpc.proxies.AuthenticatedRpcOpsProxy
|
||||||
import net.corda.node.internal.rpc.proxies.ThreadContextAdjustingRpcOpsProxy
|
import net.corda.node.internal.rpc.proxies.ThreadContextAdjustingRpcOpsProxy
|
||||||
|
import net.corda.node.internal.shell.InteractiveShell
|
||||||
import net.corda.node.services.ContractUpgradeHandler
|
import net.corda.node.services.ContractUpgradeHandler
|
||||||
import net.corda.node.services.FinalityHandler
|
import net.corda.node.services.FinalityHandler
|
||||||
import net.corda.node.services.NotaryChangeHandler
|
import net.corda.node.services.NotaryChangeHandler
|
||||||
@ -99,8 +100,7 @@ import net.corda.node.services.api.WritableTransactionStorage
|
|||||||
import net.corda.node.services.attachments.NodeAttachmentTrustCalculator
|
import net.corda.node.services.attachments.NodeAttachmentTrustCalculator
|
||||||
import net.corda.node.services.config.NodeConfiguration
|
import net.corda.node.services.config.NodeConfiguration
|
||||||
import net.corda.node.services.config.rpc.NodeRpcOptions
|
import net.corda.node.services.config.rpc.NodeRpcOptions
|
||||||
import net.corda.node.services.config.shell.determineUnsafeUsers
|
import net.corda.node.services.config.shell.toShellConfigMap
|
||||||
import net.corda.node.services.config.shell.toShellConfig
|
|
||||||
import net.corda.node.services.config.shouldInitCrashShell
|
import net.corda.node.services.config.shouldInitCrashShell
|
||||||
import net.corda.node.services.diagnostics.NodeDiagnosticsService
|
import net.corda.node.services.diagnostics.NodeDiagnosticsService
|
||||||
import net.corda.node.services.events.NodeSchedulerService
|
import net.corda.node.services.events.NodeSchedulerService
|
||||||
@ -166,7 +166,6 @@ import net.corda.nodeapi.internal.persistence.RestrictedEntityManager
|
|||||||
import net.corda.nodeapi.internal.persistence.SchemaMigration
|
import net.corda.nodeapi.internal.persistence.SchemaMigration
|
||||||
import net.corda.nodeapi.internal.persistence.contextDatabase
|
import net.corda.nodeapi.internal.persistence.contextDatabase
|
||||||
import net.corda.nodeapi.internal.persistence.withoutDatabaseAccess
|
import net.corda.nodeapi.internal.persistence.withoutDatabaseAccess
|
||||||
import net.corda.tools.shell.InteractiveShell
|
|
||||||
import org.apache.activemq.artemis.utils.ReusableLatch
|
import org.apache.activemq.artemis.utils.ReusableLatch
|
||||||
import org.jolokia.jvmagent.JolokiaServer
|
import org.jolokia.jvmagent.JolokiaServer
|
||||||
import org.jolokia.jvmagent.JolokiaServerConfig
|
import org.jolokia.jvmagent.JolokiaServerConfig
|
||||||
@ -689,16 +688,11 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
|||||||
|
|
||||||
open fun startShell() {
|
open fun startShell() {
|
||||||
if (configuration.shouldInitCrashShell()) {
|
if (configuration.shouldInitCrashShell()) {
|
||||||
val shellConfiguration = configuration.toShellConfig()
|
val shellConfiguration = configuration.toShellConfigMap()
|
||||||
shellConfiguration.sshdPort?.let {
|
shellConfiguration["sshdPort"]?.let {
|
||||||
log.info("Binding Shell SSHD server on port $it.")
|
log.info("Binding Shell SSHD server on port $it.")
|
||||||
}
|
}
|
||||||
|
InteractiveShell.startShellIfInstalled(configuration, shellConfiguration, cordappLoader)
|
||||||
val unsafeUsers = determineUnsafeUsers(configuration)
|
|
||||||
org.crsh.ssh.term.CRaSHCommand.setUserInfo(unsafeUsers, true, false)
|
|
||||||
log.info("Setting unsafe users as: ${unsafeUsers}")
|
|
||||||
|
|
||||||
InteractiveShell.startShell(shellConfiguration, cordappLoader.appClassLoader)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,21 +8,42 @@ import net.corda.cliutils.printError
|
|||||||
import net.corda.common.logging.CordaVersion
|
import net.corda.common.logging.CordaVersion
|
||||||
import net.corda.common.logging.errorReporting.CordaErrorContextProvider
|
import net.corda.common.logging.errorReporting.CordaErrorContextProvider
|
||||||
import net.corda.common.logging.errorReporting.ErrorCode
|
import net.corda.common.logging.errorReporting.ErrorCode
|
||||||
|
import net.corda.common.logging.errorReporting.ErrorReporting
|
||||||
|
import net.corda.common.logging.errorReporting.report
|
||||||
import net.corda.core.contracts.HashAttachmentConstraint
|
import net.corda.core.contracts.HashAttachmentConstraint
|
||||||
import net.corda.core.crypto.Crypto
|
import net.corda.core.crypto.Crypto
|
||||||
import net.corda.core.internal.*
|
import net.corda.core.internal.Emoji
|
||||||
|
import net.corda.core.internal.HashAgility
|
||||||
|
import net.corda.core.internal.PLATFORM_VERSION
|
||||||
import net.corda.core.internal.concurrent.thenMatch
|
import net.corda.core.internal.concurrent.thenMatch
|
||||||
import net.corda.core.internal.cordapp.CordappImpl
|
import net.corda.core.internal.cordapp.CordappImpl
|
||||||
|
import net.corda.core.internal.createDirectories
|
||||||
|
import net.corda.core.internal.div
|
||||||
import net.corda.core.internal.errors.AddressBindingException
|
import net.corda.core.internal.errors.AddressBindingException
|
||||||
|
import net.corda.core.internal.exists
|
||||||
|
import net.corda.core.internal.isDirectory
|
||||||
|
import net.corda.core.internal.location
|
||||||
|
import net.corda.core.internal.randomOrNull
|
||||||
|
import net.corda.core.internal.safeSymbolicRead
|
||||||
import net.corda.core.utilities.Try
|
import net.corda.core.utilities.Try
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
import net.corda.node.*
|
import net.corda.node.NodeCmdLineOptions
|
||||||
import net.corda.common.logging.errorReporting.ErrorReporting
|
import net.corda.node.SerialFilter
|
||||||
import net.corda.common.logging.errorReporting.report
|
import net.corda.node.SharedNodeCmdLineOptions
|
||||||
|
import net.corda.node.VersionInfo
|
||||||
|
import net.corda.node.defaultSerialFilter
|
||||||
import net.corda.node.internal.Node.Companion.isInvalidJavaVersion
|
import net.corda.node.internal.Node.Companion.isInvalidJavaVersion
|
||||||
import net.corda.node.internal.cordapp.MultipleCordappsForFlowException
|
import net.corda.node.internal.cordapp.MultipleCordappsForFlowException
|
||||||
import net.corda.node.internal.subcommands.*
|
import net.corda.node.internal.shell.InteractiveShell
|
||||||
|
import net.corda.node.internal.subcommands.ClearNetworkCacheCli
|
||||||
|
import net.corda.node.internal.subcommands.GenerateNodeInfoCli
|
||||||
|
import net.corda.node.internal.subcommands.GenerateRpcSslCertsCli
|
||||||
|
import net.corda.node.internal.subcommands.InitialRegistration
|
||||||
|
import net.corda.node.internal.subcommands.InitialRegistrationCli
|
||||||
|
import net.corda.node.internal.subcommands.RunMigrationScriptsCli
|
||||||
|
import net.corda.node.internal.subcommands.SynchroniseSchemasCli
|
||||||
|
import net.corda.node.internal.subcommands.ValidateConfigurationCli
|
||||||
import net.corda.node.internal.subcommands.ValidateConfigurationCli.Companion.logConfigurationErrors
|
import net.corda.node.internal.subcommands.ValidateConfigurationCli.Companion.logConfigurationErrors
|
||||||
import net.corda.node.internal.subcommands.ValidateConfigurationCli.Companion.logRawConfig
|
import net.corda.node.internal.subcommands.ValidateConfigurationCli.Companion.logRawConfig
|
||||||
import net.corda.node.services.config.NodeConfiguration
|
import net.corda.node.services.config.NodeConfiguration
|
||||||
@ -33,13 +54,11 @@ import net.corda.nodeapi.internal.JVMAgentUtilities
|
|||||||
import net.corda.nodeapi.internal.addShutdownHook
|
import net.corda.nodeapi.internal.addShutdownHook
|
||||||
import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException
|
import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException
|
||||||
import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException
|
import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException
|
||||||
import net.corda.tools.shell.InteractiveShell
|
|
||||||
import org.fusesource.jansi.Ansi
|
import org.fusesource.jansi.Ansi
|
||||||
import org.slf4j.bridge.SLF4JBridgeHandler
|
import org.slf4j.bridge.SLF4JBridgeHandler
|
||||||
import picocli.CommandLine.Mixin
|
import picocli.CommandLine.Mixin
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.RandomAccessFile
|
import java.io.RandomAccessFile
|
||||||
import java.lang.NullPointerException
|
|
||||||
import java.lang.management.ManagementFactory
|
import java.lang.management.ManagementFactory
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.nio.channels.UnresolvedAddressException
|
import java.nio.channels.UnresolvedAddressException
|
||||||
@ -236,29 +255,25 @@ open class NodeStartup : NodeStartupLogging {
|
|||||||
val loadedCodapps = node.services.cordappProvider.cordapps.filter { it.isLoaded }
|
val loadedCodapps = node.services.cordappProvider.cordapps.filter { it.isLoaded }
|
||||||
logLoadedCorDapps(loadedCodapps)
|
logLoadedCorDapps(loadedCodapps)
|
||||||
|
|
||||||
node.nodeReadyFuture.thenMatch({
|
node.nodeReadyFuture.thenMatch(
|
||||||
// Elapsed time in seconds. We used 10 / 100.0 and not directly / 1000.0 to only keep two decimal digits.
|
{
|
||||||
val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0
|
// Elapsed time in seconds. We used 10 / 100.0 and not directly / 1000.0 to only keep two decimal digits.
|
||||||
val name = nodeInfo.legalIdentitiesAndCerts.first().name.organisation
|
val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0
|
||||||
Node.printBasicNodeInfo("Node for \"$name\" started up and registered in $elapsed sec")
|
val name = nodeInfo.legalIdentitiesAndCerts.first().name.organisation
|
||||||
|
Node.printBasicNodeInfo("Node for \"$name\" started up and registered in $elapsed sec")
|
||||||
|
|
||||||
// Don't start the shell if there's no console attached.
|
// Don't start the shell if there's no console attached.
|
||||||
if (node.configuration.shouldStartLocalShell()) {
|
// Look for shell here??
|
||||||
node.startupComplete.then {
|
if (node.configuration.shouldStartLocalShell()) {
|
||||||
try {
|
InteractiveShell.runLocalShellIfInstalled(node.configuration.baseDirectory, node::stop)
|
||||||
InteractiveShell.runLocalShell(node::stop)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logger.error("Shell failed to start", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
if (node.configuration.shouldStartSSHDaemon()) {
|
||||||
if (node.configuration.shouldStartSSHDaemon()) {
|
Node.printBasicNodeInfo("SSH server listening on port", node.configuration.sshd!!.port.toString())
|
||||||
Node.printBasicNodeInfo("SSH server listening on port", node.configuration.sshd!!.port.toString())
|
}
|
||||||
}
|
},
|
||||||
},
|
{ th ->
|
||||||
{ th ->
|
logger.error("Unexpected exception during registration", th)
|
||||||
logger.error("Unexpected exception during registration", th)
|
})
|
||||||
})
|
|
||||||
node.run()
|
node.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,98 @@
|
|||||||
|
package net.corda.node.internal.shell
|
||||||
|
|
||||||
|
import net.corda.node.services.config.NodeConfiguration
|
||||||
|
import net.corda.node.services.config.shell.determineUnsafeUsers
|
||||||
|
import net.corda.nodeapi.internal.cordapp.CordappLoader
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.io.File
|
||||||
|
import java.net.URLClassLoader
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
|
object InteractiveShell {
|
||||||
|
|
||||||
|
private val log = LoggerFactory.getLogger(InteractiveShell::class.java)
|
||||||
|
|
||||||
|
private const val INTERACTIVE_SHELL_CLASS = "net.corda.tools.shell.InteractiveShell"
|
||||||
|
private const val CRASH_COMMAND_CLASS = "org.crsh.ssh.term.CRaSHCommand"
|
||||||
|
|
||||||
|
private const val START_SHELL_METHOD = "startShell"
|
||||||
|
private const val RUN_LOCAL_SHELL_METHOD = "runLocalShell"
|
||||||
|
private const val SET_USER_INFO_METHOD = "setUserInfo"
|
||||||
|
|
||||||
|
fun startShellIfInstalled(configuration: NodeConfiguration, shellConfiguration: Map<String, Any?>, cordappLoader: CordappLoader) {
|
||||||
|
val shellJar = getSingleShellJarInDriversDirectory(configuration.baseDirectory)
|
||||||
|
if (shellJar != null) {
|
||||||
|
try {
|
||||||
|
val classLoader = URLClassLoader(arrayOf(shellJar.toPath().toUri().toURL()), javaClass.classLoader)
|
||||||
|
setUnsafeUsers(classLoader, configuration)
|
||||||
|
startShell(classLoader, shellConfiguration, cordappLoader)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.error("Shell failed to start", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only call this after [startShellIfInstalled] has been called or the required classes will not be loaded into the current classloader.
|
||||||
|
*/
|
||||||
|
fun runLocalShellIfInstalled(baseDirectory: Path, onExit: () -> Unit = {}) {
|
||||||
|
val shellJar = getSingleShellJarInDriversDirectory(baseDirectory)
|
||||||
|
if (shellJar != null) {
|
||||||
|
try {
|
||||||
|
runLocalShell(javaClass.classLoader, onExit)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.error("Shell failed to start", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSingleShellJarInDriversDirectory(baseDirectory: Path): File? {
|
||||||
|
val uriToDriversDirectory = Paths.get("${baseDirectory}/drivers").toUri()
|
||||||
|
val files = File(uriToDriversDirectory)
|
||||||
|
.listFiles()
|
||||||
|
?.filter { "corda-shell" in it.name }
|
||||||
|
?.filter { "jar" == it.extension }
|
||||||
|
?: emptyList()
|
||||||
|
|
||||||
|
return if (files.isNotEmpty()) {
|
||||||
|
check(files.size == 1) {
|
||||||
|
("More than one corda-shell jar installed in /drivers directory. " +
|
||||||
|
"Remove all corda-shell jars except for the one that should be used").also {
|
||||||
|
log.error(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files.single()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setUnsafeUsers(classLoader: ClassLoader, configuration: NodeConfiguration) {
|
||||||
|
val unsafeUsers = determineUnsafeUsers(configuration)
|
||||||
|
val clazz = classLoader.loadClass(CRASH_COMMAND_CLASS)
|
||||||
|
clazz.getDeclaredMethod(SET_USER_INFO_METHOD, Set::class.java, Boolean::class.java, Boolean::class.java)
|
||||||
|
.invoke(null, unsafeUsers, true, false)
|
||||||
|
log.info("Setting unsafe users as: $unsafeUsers")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startShell(classLoader: ClassLoader, shellConfiguration: Map<String, Any?>, cordappLoader: CordappLoader) {
|
||||||
|
val clazz = classLoader.loadClass(INTERACTIVE_SHELL_CLASS)
|
||||||
|
val instance = clazz.getDeclaredConstructor()
|
||||||
|
.apply { this.isAccessible = true }
|
||||||
|
.newInstance()
|
||||||
|
clazz.getDeclaredMethod(START_SHELL_METHOD, Map::class.java, ClassLoader::class.java, Boolean::class.java)
|
||||||
|
.invoke(instance, shellConfiguration, cordappLoader.appClassLoader, false)
|
||||||
|
log.info("INTERACTIVE SHELL STARTED ABSTRACT NODE")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runLocalShell(classLoader: ClassLoader, onExit: () -> Unit = {}) {
|
||||||
|
val clazz = classLoader.loadClass(INTERACTIVE_SHELL_CLASS)
|
||||||
|
// Gets the existing instance created by [startShell] as [InteractiveShell] is a static instance
|
||||||
|
val instance = clazz.getDeclaredConstructor()
|
||||||
|
.apply { this.isAccessible = true }
|
||||||
|
.newInstance()
|
||||||
|
clazz.getDeclaredMethod(RUN_LOCAL_SHELL_METHOD, Function0::class.java).invoke(instance, onExit)
|
||||||
|
log.info("INTERACTIVE SHELL STARTED")
|
||||||
|
}
|
||||||
|
}
|
@ -11,17 +11,18 @@ import net.corda.core.internal.notary.NotaryServiceFlow
|
|||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
import net.corda.node.services.config.rpc.NodeRpcOptions
|
import net.corda.node.services.config.rpc.NodeRpcOptions
|
||||||
import net.corda.node.services.config.schema.v1.V1NodeConfigurationSpec
|
import net.corda.node.services.config.schema.v1.V1NodeConfigurationSpec
|
||||||
|
import net.corda.node.services.config.shell.SSHDConfiguration
|
||||||
import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier
|
import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier
|
||||||
import net.corda.nodeapi.internal.config.MutualSslConfiguration
|
import net.corda.nodeapi.internal.config.MutualSslConfiguration
|
||||||
import net.corda.nodeapi.internal.config.User
|
import net.corda.nodeapi.internal.config.User
|
||||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||||
import net.corda.notary.experimental.bftsmart.BFTSmartConfig
|
import net.corda.notary.experimental.bftsmart.BFTSmartConfig
|
||||||
import net.corda.notary.experimental.raft.RaftConfig
|
import net.corda.notary.experimental.raft.RaftConfig
|
||||||
import net.corda.tools.shell.SSHDConfiguration
|
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.util.*
|
import java.util.Properties
|
||||||
|
import java.util.UUID
|
||||||
import javax.security.auth.x500.X500Principal
|
import javax.security.auth.x500.X500Principal
|
||||||
|
|
||||||
val Int.MB: Long get() = this * 1024L * 1024L
|
val Int.MB: Long get() = this * 1024L * 1024L
|
||||||
|
@ -8,6 +8,7 @@ import net.corda.core.utilities.NetworkHostAndPort
|
|||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
import net.corda.core.utilities.seconds
|
import net.corda.core.utilities.seconds
|
||||||
import net.corda.node.services.config.rpc.NodeRpcOptions
|
import net.corda.node.services.config.rpc.NodeRpcOptions
|
||||||
|
import net.corda.node.services.config.shell.SSHDConfiguration
|
||||||
import net.corda.nodeapi.BrokerRpcSslOptions
|
import net.corda.nodeapi.BrokerRpcSslOptions
|
||||||
import net.corda.nodeapi.internal.DEV_PUB_KEY_HASHES
|
import net.corda.nodeapi.internal.DEV_PUB_KEY_HASHES
|
||||||
import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier
|
import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier
|
||||||
@ -15,11 +16,11 @@ import net.corda.nodeapi.internal.config.MutualSslConfiguration
|
|||||||
import net.corda.nodeapi.internal.config.SslConfiguration
|
import net.corda.nodeapi.internal.config.SslConfiguration
|
||||||
import net.corda.nodeapi.internal.config.User
|
import net.corda.nodeapi.internal.config.User
|
||||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||||
import net.corda.tools.shell.SSHDConfiguration
|
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.util.*
|
import java.util.Properties
|
||||||
|
import java.util.UUID
|
||||||
import javax.security.auth.x500.X500Principal
|
import javax.security.auth.x500.X500Principal
|
||||||
|
|
||||||
data class NodeConfigurationImpl(
|
data class NodeConfigurationImpl(
|
||||||
|
@ -40,13 +40,13 @@ import net.corda.node.services.config.schema.parsers.toProperties
|
|||||||
import net.corda.node.services.config.schema.parsers.toURL
|
import net.corda.node.services.config.schema.parsers.toURL
|
||||||
import net.corda.node.services.config.schema.parsers.toUUID
|
import net.corda.node.services.config.schema.parsers.toUUID
|
||||||
import net.corda.node.services.config.schema.parsers.validValue
|
import net.corda.node.services.config.schema.parsers.validValue
|
||||||
|
import net.corda.node.services.config.shell.SSHDConfiguration
|
||||||
import net.corda.nodeapi.BrokerRpcSslOptions
|
import net.corda.nodeapi.BrokerRpcSslOptions
|
||||||
import net.corda.nodeapi.internal.config.User
|
import net.corda.nodeapi.internal.config.User
|
||||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||||
import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel
|
import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel
|
||||||
import net.corda.notary.experimental.bftsmart.BFTSmartConfig
|
import net.corda.notary.experimental.bftsmart.BFTSmartConfig
|
||||||
import net.corda.notary.experimental.raft.RaftConfig
|
import net.corda.notary.experimental.raft.RaftConfig
|
||||||
import net.corda.tools.shell.SSHDConfiguration
|
|
||||||
|
|
||||||
internal object UserSpec : Configuration.Specification<User>("User") {
|
internal object UserSpec : Configuration.Specification<User>("User") {
|
||||||
private val username by string().optional()
|
private val username by string().optional()
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package net.corda.tools.shell
|
package net.corda.node.services.config.shell
|
||||||
|
|
||||||
data class SSHDConfiguration(val port: Int) {
|
data class SSHDConfiguration(val port: Int) {
|
||||||
companion object {
|
companion object {
|
||||||
@ -11,7 +11,7 @@ data class SSHDConfiguration(val port: Int) {
|
|||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun parse(str: String): SSHDConfiguration {
|
fun parse(str: String): SSHDConfiguration {
|
||||||
require(!str.isBlank()) { SSHDConfiguration.MISSING_PORT_FORMAT.format(str) }
|
require(str.isNotBlank()) { MISSING_PORT_FORMAT.format(str) }
|
||||||
val port = try {
|
val port = try {
|
||||||
str.toInt()
|
str.toInt()
|
||||||
} catch (ex: NumberFormatException) {
|
} catch (ex: NumberFormatException) {
|
@ -3,22 +3,23 @@ package net.corda.node.services.config.shell
|
|||||||
import net.corda.core.internal.div
|
import net.corda.core.internal.div
|
||||||
import net.corda.node.internal.clientSslOptionsCompatibleWith
|
import net.corda.node.internal.clientSslOptionsCompatibleWith
|
||||||
import net.corda.node.services.config.NodeConfiguration
|
import net.corda.node.services.config.NodeConfiguration
|
||||||
import net.corda.tools.shell.ShellConfiguration
|
|
||||||
import net.corda.tools.shell.ShellConfiguration.Companion.COMMANDS_DIR
|
private const val COMMANDS_DIR = "shell-commands"
|
||||||
import net.corda.tools.shell.ShellConfiguration.Companion.CORDAPPS_DIR
|
private const val CORDAPPS_DIR = "cordapps"
|
||||||
import net.corda.tools.shell.ShellConfiguration.Companion.SSHD_HOSTKEY_DIR
|
private const val SSHD_HOSTKEY_DIR = "ssh"
|
||||||
|
|
||||||
//re-packs data to Shell specific classes
|
//re-packs data to Shell specific classes
|
||||||
fun NodeConfiguration.toShellConfig() = ShellConfiguration(
|
fun NodeConfiguration.toShellConfigMap() = mapOf(
|
||||||
commandsDirectory = this.baseDirectory / COMMANDS_DIR,
|
"commandsDirectory" to this.baseDirectory / COMMANDS_DIR,
|
||||||
cordappsDirectory = this.baseDirectory.toString() / CORDAPPS_DIR,
|
"cordappsDirectory" to this.baseDirectory.toString() / CORDAPPS_DIR,
|
||||||
user = INTERNAL_SHELL_USER,
|
"user" to INTERNAL_SHELL_USER,
|
||||||
password = internalShellPassword,
|
"password" to internalShellPassword,
|
||||||
permissions = internalShellPermissions(!this.localShellUnsafe),
|
"permissions" to internalShellPermissions(!this.localShellUnsafe),
|
||||||
localShellAllowExitInSafeMode = this.localShellAllowExitInSafeMode,
|
"localShellAllowExitInSafeMode" to this.localShellAllowExitInSafeMode,
|
||||||
localShellUnsafe = this.localShellUnsafe,
|
"localShellUnsafe" to this.localShellUnsafe,
|
||||||
hostAndPort = this.rpcOptions.address,
|
"hostAndPort" to this.rpcOptions.address,
|
||||||
ssl = clientSslOptionsCompatibleWith(this.rpcOptions),
|
"ssl" to clientSslOptionsCompatibleWith(this.rpcOptions),
|
||||||
sshdPort = this.sshd?.port,
|
"sshdPort" to this.sshd?.port,
|
||||||
sshHostKeyDirectory = this.baseDirectory / SSHD_HOSTKEY_DIR,
|
"sshHostKeyDirectory" to this.baseDirectory / SSHD_HOSTKEY_DIR,
|
||||||
noLocalShell = this.noLocalShell)
|
"noLocalShell" to this.noLocalShell
|
||||||
|
)
|
||||||
|
@ -10,10 +10,10 @@ import net.corda.core.internal.div
|
|||||||
import net.corda.core.internal.toPath
|
import net.corda.core.internal.toPath
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
import net.corda.core.utilities.seconds
|
import net.corda.core.utilities.seconds
|
||||||
|
import net.corda.node.services.config.shell.SSHDConfiguration
|
||||||
import net.corda.nodeapi.internal.config.getBooleanCaseInsensitive
|
import net.corda.nodeapi.internal.config.getBooleanCaseInsensitive
|
||||||
import net.corda.testing.core.ALICE_NAME
|
import net.corda.testing.core.ALICE_NAME
|
||||||
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
||||||
import net.corda.tools.shell.SSHDConfiguration
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
@ -73,8 +73,6 @@ include 'tools:loadtest'
|
|||||||
include 'tools:graphs'
|
include 'tools:graphs'
|
||||||
include 'tools:bootstrapper'
|
include 'tools:bootstrapper'
|
||||||
include 'tools:blobinspector'
|
include 'tools:blobinspector'
|
||||||
include 'tools:shell'
|
|
||||||
include 'tools:shell-cli'
|
|
||||||
include 'tools:network-builder'
|
include 'tools:network-builder'
|
||||||
include 'tools:cliutils'
|
include 'tools:cliutils'
|
||||||
include 'tools:worldmap'
|
include 'tools:worldmap'
|
||||||
|
@ -27,6 +27,9 @@ sourceSets {
|
|||||||
dependencies {
|
dependencies {
|
||||||
compile project(':test-utils')
|
compile project(':test-utils')
|
||||||
|
|
||||||
|
compile group: 'org.apache.sshd', name: 'sshd-common', version: '2.3.0'
|
||||||
|
// integrationTestRuntime group: 'org.apache.sshd', name: 'sshd-common', version: '2.3.0'
|
||||||
|
|
||||||
// Integration test helpers
|
// Integration test helpers
|
||||||
testCompile "org.assertj:assertj-core:$assertj_version"
|
testCompile "org.assertj:assertj-core:$assertj_version"
|
||||||
integrationTestImplementation "junit:junit:$junit_version"
|
integrationTestImplementation "junit:junit:$junit_version"
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
Standalone Shell
|
|
||||||
----------------
|
|
||||||
|
|
||||||
Documentation for the standalone shell can be found [here](https://docs.corda.net/head/shell.html#the-standalone-shell)
|
|
||||||
|
|
||||||
To build this from the command line on Unix or MacOS:
|
|
||||||
|
|
||||||
Run ``./gradlew tools:shell-cli:buildShellCli`` to create a fat JAR in ``tools/shell-cli/build/libs``
|
|
||||||
|
|
||||||
To build this from the command line on Windows:
|
|
||||||
|
|
||||||
Run ``gradlew tools:shell-cli:buildShellCli`` to create a fat JAR in ``tools/shell-cli/build/libs``
|
|
@ -1,51 +0,0 @@
|
|||||||
description 'Corda Shell CLI'
|
|
||||||
|
|
||||||
apply plugin: 'application'
|
|
||||||
// We need to set mainClassName before applying the shadow plugin.
|
|
||||||
mainClassName = 'net.corda.tools.shell.StandaloneShellKt'
|
|
||||||
|
|
||||||
apply plugin: 'com.github.johnrengelman.shadow'
|
|
||||||
apply plugin: 'net.corda.plugins.publish-utils'
|
|
||||||
apply plugin: 'com.jfrog.artifactory'
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
compile project(':tools:shell')
|
|
||||||
compile project(':tools:cliutils')
|
|
||||||
compile project(":common-logging")
|
|
||||||
compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
|
|
||||||
compile "org.slf4j:jul-to-slf4j:$slf4j_version"
|
|
||||||
|
|
||||||
testCompile(project(':test-utils')) {
|
|
||||||
exclude group: 'org.apache.logging.log4j', module: 'log4j-slf4j-impl'
|
|
||||||
}
|
|
||||||
|
|
||||||
testCompile(project(':test-cli'))
|
|
||||||
}
|
|
||||||
|
|
||||||
processResources {
|
|
||||||
from file("$rootDir/config/dev/log4j2.xml")
|
|
||||||
}
|
|
||||||
|
|
||||||
shadowJar {
|
|
||||||
archiveClassifier = jdkClassifier
|
|
||||||
mergeServiceFiles()
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register('buildShellCli') {
|
|
||||||
dependsOn shadowJar
|
|
||||||
}
|
|
||||||
|
|
||||||
artifacts {
|
|
||||||
archives shadowJar
|
|
||||||
publish shadowJar
|
|
||||||
}
|
|
||||||
|
|
||||||
jar {
|
|
||||||
archiveClassifier = "ignore"
|
|
||||||
enabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
publish {
|
|
||||||
disableDefaultJar = true
|
|
||||||
name 'corda-tools-shell-cli'
|
|
||||||
}
|
|
@ -1,169 +0,0 @@
|
|||||||
package net.corda.tools.shell
|
|
||||||
|
|
||||||
import com.typesafe.config.Config
|
|
||||||
import com.typesafe.config.ConfigFactory
|
|
||||||
import net.corda.core.internal.div
|
|
||||||
import net.corda.core.messaging.ClientRpcSslOptions
|
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
|
||||||
import net.corda.nodeapi.internal.config.parseAs
|
|
||||||
import net.corda.tools.shell.ShellConfiguration.Companion.COMMANDS_DIR
|
|
||||||
import picocli.CommandLine.Option
|
|
||||||
import java.nio.file.Path
|
|
||||||
import java.nio.file.Paths
|
|
||||||
|
|
||||||
class ShellCmdLineOptions {
|
|
||||||
@Option(
|
|
||||||
names = ["-f", "--config-file"],
|
|
||||||
description = ["The path to the shell configuration file, used instead of providing the rest of command line options."]
|
|
||||||
)
|
|
||||||
var configFile: Path? = null
|
|
||||||
|
|
||||||
@Option(
|
|
||||||
names = ["-c", "--cordapp-directory"],
|
|
||||||
description = ["The path to the directory containing CorDapp JARs, CorDapps are required when starting flows."]
|
|
||||||
)
|
|
||||||
var cordappDirectory: Path? = null
|
|
||||||
|
|
||||||
@Option(
|
|
||||||
names = ["-o", "--commands-directory"],
|
|
||||||
description = ["The path to the directory containing additional CRaSH shell commands."]
|
|
||||||
)
|
|
||||||
var commandsDirectory: Path? = null
|
|
||||||
|
|
||||||
@Option(
|
|
||||||
names = ["-a", "--host"],
|
|
||||||
description = ["The host address of the Corda node."]
|
|
||||||
)
|
|
||||||
var host: String? = null
|
|
||||||
|
|
||||||
@Option(
|
|
||||||
names = ["-p", "--port"],
|
|
||||||
description = ["The RPC port of the Corda node."]
|
|
||||||
)
|
|
||||||
var port: String? = null
|
|
||||||
|
|
||||||
@Option(
|
|
||||||
names = ["--user"],
|
|
||||||
description = ["The RPC user name."]
|
|
||||||
)
|
|
||||||
var user: String? = null
|
|
||||||
|
|
||||||
@Option(
|
|
||||||
names = ["--password"],
|
|
||||||
description = ["The RPC user password."]
|
|
||||||
)
|
|
||||||
var password: String? = null
|
|
||||||
|
|
||||||
|
|
||||||
@Option(
|
|
||||||
names = ["--truststore-password"],
|
|
||||||
description = ["The password to unlock the TrustStore file."]
|
|
||||||
)
|
|
||||||
var trustStorePassword: String? = null
|
|
||||||
|
|
||||||
|
|
||||||
@Option(
|
|
||||||
names = ["--truststore-file"],
|
|
||||||
description = ["The path to the TrustStore file."]
|
|
||||||
)
|
|
||||||
var trustStoreFile: Path? = null
|
|
||||||
|
|
||||||
|
|
||||||
@Option(
|
|
||||||
names = ["--truststore-type"],
|
|
||||||
description = ["The type of the TrustStore (e.g. JKS)."]
|
|
||||||
)
|
|
||||||
var trustStoreType: String? = null
|
|
||||||
|
|
||||||
|
|
||||||
private fun toConfigFile(): Config {
|
|
||||||
val cmdOpts = mutableMapOf<String, Any?>()
|
|
||||||
|
|
||||||
commandsDirectory?.apply { cmdOpts["extensions.commands.path"] = this.toString() }
|
|
||||||
cordappDirectory?.apply { cmdOpts["extensions.cordapps.path"] = this.toString() }
|
|
||||||
user?.apply { cmdOpts["node.user"] = this }
|
|
||||||
password?.apply { cmdOpts["node.password"] = this }
|
|
||||||
host?.apply { cmdOpts["node.addresses.rpc.host"] = this }
|
|
||||||
port?.apply { cmdOpts["node.addresses.rpc.port"] = this }
|
|
||||||
trustStoreFile?.apply { cmdOpts["ssl.truststore.path"] = this.toString() }
|
|
||||||
trustStorePassword?.apply { cmdOpts["ssl.truststore.password"] = this }
|
|
||||||
trustStoreType?.apply { cmdOpts["ssl.truststore.type"] = this }
|
|
||||||
|
|
||||||
return ConfigFactory.parseMap(cmdOpts)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return configuration parsed from an optional config file (provided by the command line option)
|
|
||||||
* and then overridden by the command line options */
|
|
||||||
fun toConfig(): ShellConfiguration {
|
|
||||||
val fileConfig = configFile?.let { ConfigFactory.parseFile(it.toFile()) }
|
|
||||||
?: ConfigFactory.empty()
|
|
||||||
val typeSafeConfig = toConfigFile().withFallback(fileConfig).resolve()
|
|
||||||
val shellConfigFile = typeSafeConfig.parseAs<ShellConfigurationFile.ShellConfigFile>()
|
|
||||||
return shellConfigFile.toShellConfiguration()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Object representation of Shell configuration file */
|
|
||||||
private class ShellConfigurationFile {
|
|
||||||
data class Rpc(
|
|
||||||
val host: String,
|
|
||||||
val port: Int)
|
|
||||||
|
|
||||||
data class Addresses(
|
|
||||||
val rpc: Rpc
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Node(
|
|
||||||
val addresses: Addresses,
|
|
||||||
val user: String?,
|
|
||||||
val password: String?
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Cordapps(
|
|
||||||
val path: String
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Commands(
|
|
||||||
val path: String
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Extensions(
|
|
||||||
val cordapps: Cordapps?,
|
|
||||||
val commands: Commands?
|
|
||||||
)
|
|
||||||
|
|
||||||
data class KeyStore(
|
|
||||||
val path: String,
|
|
||||||
val type: String = "JKS",
|
|
||||||
val password: String
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Ssl(
|
|
||||||
val truststore: KeyStore
|
|
||||||
)
|
|
||||||
|
|
||||||
data class ShellConfigFile(
|
|
||||||
val node: Node,
|
|
||||||
val extensions: Extensions?,
|
|
||||||
val ssl: Ssl?
|
|
||||||
) {
|
|
||||||
fun toShellConfiguration(): ShellConfiguration {
|
|
||||||
|
|
||||||
val sslOptions =
|
|
||||||
ssl?.let {
|
|
||||||
ClientRpcSslOptions(
|
|
||||||
trustStorePath = Paths.get(it.truststore.path),
|
|
||||||
trustStorePassword = it.truststore.password)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ShellConfiguration(
|
|
||||||
commandsDirectory = extensions?.commands?.let { Paths.get(it.path) } ?: Paths.get(".")
|
|
||||||
/ COMMANDS_DIR,
|
|
||||||
cordappsDirectory = extensions?.cordapps?.let { Paths.get(it.path) },
|
|
||||||
user = node.user ?: "",
|
|
||||||
password = node.password ?: "",
|
|
||||||
hostAndPort = NetworkHostAndPort(node.addresses.rpc.host, node.addresses.rpc.port),
|
|
||||||
ssl = sslOptions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,113 +0,0 @@
|
|||||||
package net.corda.tools.shell
|
|
||||||
|
|
||||||
import com.jcabi.manifests.Manifests
|
|
||||||
import net.corda.cliutils.CordaCliWrapper
|
|
||||||
import net.corda.cliutils.ExitCodes
|
|
||||||
import net.corda.cliutils.start
|
|
||||||
import net.corda.core.internal.exists
|
|
||||||
import net.corda.core.internal.isRegularFile
|
|
||||||
import net.corda.core.internal.list
|
|
||||||
import org.fusesource.jansi.Ansi
|
|
||||||
import org.fusesource.jansi.AnsiConsole
|
|
||||||
import org.slf4j.bridge.SLF4JBridgeHandler
|
|
||||||
import picocli.CommandLine.Mixin
|
|
||||||
import java.io.BufferedReader
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStreamReader
|
|
||||||
import java.net.URL
|
|
||||||
import java.net.URLClassLoader
|
|
||||||
import java.nio.file.Path
|
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
import kotlin.streams.toList
|
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
StandaloneShell().start(args)
|
|
||||||
}
|
|
||||||
|
|
||||||
class StandaloneShell : CordaCliWrapper("corda-shell", "The Corda standalone shell.") {
|
|
||||||
@Mixin
|
|
||||||
var cmdLineOptions = ShellCmdLineOptions()
|
|
||||||
|
|
||||||
lateinit var configuration: ShellConfiguration
|
|
||||||
|
|
||||||
private fun getCordappsInDirectory(cordappsDir: Path?): List<URL> =
|
|
||||||
if (cordappsDir == null || !cordappsDir.exists()) {
|
|
||||||
emptyList()
|
|
||||||
} else {
|
|
||||||
cordappsDir.list {
|
|
||||||
it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.map { it.toUri().toURL() }.toList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Workaround in case console is not available
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun readLine(format: String, vararg args: Any): String {
|
|
||||||
if (System.console() != null) {
|
|
||||||
return System.console().readLine(format, *args)
|
|
||||||
}
|
|
||||||
print(String.format(format, *args))
|
|
||||||
val reader = BufferedReader(InputStreamReader(System.`in`))
|
|
||||||
return reader.readLine()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun readPassword(format: String, vararg args: Any) =
|
|
||||||
if (System.console() != null) System.console().readPassword(format, *args) else this.readLine(format, *args).toCharArray()
|
|
||||||
|
|
||||||
private fun getManifestEntry(key: String) = if (Manifests.exists(key)) Manifests.read(key) else "Unknown"
|
|
||||||
|
|
||||||
override fun initLogging() : Boolean {
|
|
||||||
super.initLogging()
|
|
||||||
SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler.
|
|
||||||
SLF4JBridgeHandler.install()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun runProgram(): Int {
|
|
||||||
configuration = try {
|
|
||||||
cmdLineOptions.toConfig()
|
|
||||||
} catch(e: Exception) {
|
|
||||||
println("Configuration exception: ${e.message}")
|
|
||||||
return ExitCodes.FAILURE
|
|
||||||
}
|
|
||||||
|
|
||||||
val cordappJarPaths = getCordappsInDirectory(configuration.cordappsDirectory)
|
|
||||||
val classLoader: ClassLoader = URLClassLoader(cordappJarPaths.toTypedArray(), javaClass.classLoader)
|
|
||||||
with(configuration) {
|
|
||||||
if (user.isEmpty()) {
|
|
||||||
user = readLine("User:")
|
|
||||||
}
|
|
||||||
if (password.isEmpty()) {
|
|
||||||
password = String(readPassword("Password:"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
InteractiveShell.startShell(configuration, classLoader, true)
|
|
||||||
try {
|
|
||||||
//connecting to node by requesting node info to fail fast
|
|
||||||
InteractiveShell.nodeInfo()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("Cannot login to ${configuration.hostAndPort}, reason: \"${e.message}\"")
|
|
||||||
return ExitCodes.FAILURE
|
|
||||||
}
|
|
||||||
|
|
||||||
val exit = CountDownLatch(1)
|
|
||||||
AnsiConsole.systemInstall()
|
|
||||||
println(Ansi.ansi().fgBrightRed().a(
|
|
||||||
""" ______ __""").newline().a(
|
|
||||||
""" / ____/ _________/ /___ _""").newline().a(
|
|
||||||
""" / / __ / ___/ __ / __ `/ """).newline().fgBrightRed().a(
|
|
||||||
"""/ /___ /_/ / / / /_/ / /_/ /""").newline().fgBrightRed().a(
|
|
||||||
"""\____/ /_/ \__,_/\__,_/""").reset().fgBrightDefault().bold()
|
|
||||||
.newline().a("--- ${getManifestEntry("Corda-Vendor")} ${getManifestEntry("Corda-Release-Version")} (${getManifestEntry("Corda-Revision").take(7)}) ---")
|
|
||||||
.newline()
|
|
||||||
.newline().a("Standalone Shell connected to ${configuration.hostAndPort}")
|
|
||||||
.reset())
|
|
||||||
InteractiveShell.runLocalShell {
|
|
||||||
exit.countDown()
|
|
||||||
}
|
|
||||||
|
|
||||||
exit.await()
|
|
||||||
// because we can't clean certain Crash Shell threads that block on read()
|
|
||||||
return ExitCodes.SUCCESS
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
package net.corda.tools.shell.base
|
|
||||||
|
|
||||||
// Note that this file MUST be in a sub-directory called "base" relative to the path
|
|
||||||
// given in the configuration code in InteractiveShell.
|
|
||||||
|
|
||||||
// Copy of the login.groovy file from 'shell' module with the welcome tailored for the standalone shell
|
|
||||||
welcome = """
|
|
||||||
|
|
||||||
Welcome to the Corda interactive shell.
|
|
||||||
Useful commands include 'help' to see what is available, and 'bye' to exit the shell.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
prompt = { ->
|
|
||||||
return "${new Date()}>>> "
|
|
||||||
}
|
|
@ -1,118 +0,0 @@
|
|||||||
package net.corda.tools.shell
|
|
||||||
|
|
||||||
import net.corda.core.internal.toPath
|
|
||||||
import net.corda.core.messaging.ClientRpcSslOptions
|
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
|
||||||
import org.junit.Test
|
|
||||||
import java.nio.file.Paths
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
|
|
||||||
class StandaloneShellArgsParserTest {
|
|
||||||
private val CONFIG_FILE = StandaloneShellArgsParserTest::class.java.getResource("/config.conf").toPath()
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun empty_args_to_cmd_options() {
|
|
||||||
val expectedOptions = ShellCmdLineOptions()
|
|
||||||
|
|
||||||
assertEquals(expectedOptions.configFile, null)
|
|
||||||
assertEquals(expectedOptions.cordappDirectory, null)
|
|
||||||
assertEquals(expectedOptions.commandsDirectory, null)
|
|
||||||
assertEquals(expectedOptions.host, null)
|
|
||||||
assertEquals(expectedOptions.port, null)
|
|
||||||
assertEquals(expectedOptions.user, null)
|
|
||||||
assertEquals(expectedOptions.password, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun args_to_config() {
|
|
||||||
val options = ShellCmdLineOptions()
|
|
||||||
options.configFile = null
|
|
||||||
options.commandsDirectory = Paths.get("/x/y/commands")
|
|
||||||
options.cordappDirectory = Paths.get("/x/y/cordapps")
|
|
||||||
options.host = "alocalhost"
|
|
||||||
options.port = "1234"
|
|
||||||
options.user = "demo"
|
|
||||||
options.password = "abcd1234"
|
|
||||||
options.trustStorePassword = "pass2"
|
|
||||||
options.trustStoreFile = Paths.get("/x/y/truststore.jks")
|
|
||||||
options.trustStoreType = "dummy"
|
|
||||||
|
|
||||||
val expectedSsl = ClientRpcSslOptions(
|
|
||||||
trustStorePath = Paths.get("/x/y/truststore.jks"),
|
|
||||||
trustStorePassword = "pass2")
|
|
||||||
val expectedConfig = ShellConfiguration(
|
|
||||||
commandsDirectory = Paths.get("/x/y/commands"),
|
|
||||||
cordappsDirectory = Paths.get("/x/y/cordapps"),
|
|
||||||
user = "demo",
|
|
||||||
password = "abcd1234",
|
|
||||||
hostAndPort = NetworkHostAndPort("alocalhost", 1234),
|
|
||||||
ssl = expectedSsl,
|
|
||||||
sshdPort = null,
|
|
||||||
sshHostKeyDirectory = null,
|
|
||||||
noLocalShell = false)
|
|
||||||
|
|
||||||
val config = options.toConfig()
|
|
||||||
|
|
||||||
assertEquals(expectedConfig, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun cmd_options_to_config_from_file() {
|
|
||||||
val options = ShellCmdLineOptions()
|
|
||||||
options.configFile = CONFIG_FILE
|
|
||||||
options.commandsDirectory = null
|
|
||||||
options.cordappDirectory = null
|
|
||||||
options.host = null
|
|
||||||
options.port = null
|
|
||||||
options.user = null
|
|
||||||
options.password = null
|
|
||||||
options.trustStorePassword = null
|
|
||||||
options.trustStoreFile = null
|
|
||||||
options.trustStoreType = null
|
|
||||||
|
|
||||||
val expectedConfig = ShellConfiguration(
|
|
||||||
commandsDirectory = Paths.get("/x/y/commands"),
|
|
||||||
cordappsDirectory = Paths.get("/x/y/cordapps"),
|
|
||||||
user = "demo",
|
|
||||||
password = "abcd1234",
|
|
||||||
hostAndPort = NetworkHostAndPort("alocalhost", 1234),
|
|
||||||
ssl = ClientRpcSslOptions(
|
|
||||||
trustStorePath = Paths.get("/x/y/truststore.jks"),
|
|
||||||
trustStorePassword = "pass2"),
|
|
||||||
sshdPort = null)
|
|
||||||
|
|
||||||
val config = options.toConfig()
|
|
||||||
|
|
||||||
assertEquals(expectedConfig, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun cmd_options_override_config_from_file() {
|
|
||||||
val options = ShellCmdLineOptions()
|
|
||||||
options.configFile = CONFIG_FILE
|
|
||||||
options.commandsDirectory = null
|
|
||||||
options.host = null
|
|
||||||
options.port = null
|
|
||||||
options.user = null
|
|
||||||
options.password = "blabla"
|
|
||||||
options.trustStorePassword = null
|
|
||||||
options.trustStoreFile = null
|
|
||||||
options.trustStoreType = null
|
|
||||||
|
|
||||||
val expectedSsl = ClientRpcSslOptions(
|
|
||||||
trustStorePath = Paths.get("/x/y/truststore.jks"),
|
|
||||||
trustStorePassword = "pass2")
|
|
||||||
val expectedConfig = ShellConfiguration(
|
|
||||||
commandsDirectory = Paths.get("/x/y/commands"),
|
|
||||||
cordappsDirectory = Paths.get("/x/y/cordapps"),
|
|
||||||
user = "demo",
|
|
||||||
password = "blabla",
|
|
||||||
hostAndPort = NetworkHostAndPort("alocalhost", 1234),
|
|
||||||
ssl = expectedSsl,
|
|
||||||
sshdPort = null)
|
|
||||||
|
|
||||||
val config = options.toConfig()
|
|
||||||
|
|
||||||
assertEquals(expectedConfig, config)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
package net.corda.tools.shell
|
|
||||||
|
|
||||||
import net.corda.testing.CliBackwardsCompatibleTest
|
|
||||||
|
|
||||||
class StandaloneShellCompatibilityTest : CliBackwardsCompatibleTest(StandaloneShell::class.java)
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
|||||||
node {
|
|
||||||
addresses {
|
|
||||||
rpc {
|
|
||||||
host : "alocalhost"
|
|
||||||
port : 1234
|
|
||||||
}
|
|
||||||
}
|
|
||||||
user : demo
|
|
||||||
password : abcd1234
|
|
||||||
}
|
|
||||||
extensions {
|
|
||||||
cordapps {
|
|
||||||
path : "/x/y/cordapps"
|
|
||||||
}
|
|
||||||
commands {
|
|
||||||
path : /x/y/commands
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ssl {
|
|
||||||
truststore {
|
|
||||||
path : "/x/y/truststore.jks"
|
|
||||||
type : "JKS"
|
|
||||||
password : "pass2"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,104 +0,0 @@
|
|||||||
- commandName: "<main class>"
|
|
||||||
positionalParams: []
|
|
||||||
params:
|
|
||||||
- parameterName: "--commands-directory"
|
|
||||||
parameterType: "java.nio.file.Path"
|
|
||||||
required: false
|
|
||||||
multiParam: true
|
|
||||||
acceptableValues: []
|
|
||||||
- parameterName: "--config-file"
|
|
||||||
parameterType: "java.nio.file.Path"
|
|
||||||
required: false
|
|
||||||
multiParam: true
|
|
||||||
acceptableValues: []
|
|
||||||
- parameterName: "--cordapp-directory"
|
|
||||||
parameterType: "java.nio.file.Path"
|
|
||||||
required: false
|
|
||||||
multiParam: true
|
|
||||||
acceptableValues: []
|
|
||||||
- parameterName: "--host"
|
|
||||||
parameterType: "java.lang.String"
|
|
||||||
required: false
|
|
||||||
multiParam: false
|
|
||||||
acceptableValues: []
|
|
||||||
- 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: "--password"
|
|
||||||
parameterType: "java.lang.String"
|
|
||||||
required: false
|
|
||||||
multiParam: false
|
|
||||||
acceptableValues: []
|
|
||||||
- parameterName: "--port"
|
|
||||||
parameterType: "java.lang.String"
|
|
||||||
required: false
|
|
||||||
multiParam: false
|
|
||||||
acceptableValues: []
|
|
||||||
- parameterName: "--truststore-file"
|
|
||||||
parameterType: "java.nio.file.Path"
|
|
||||||
required: false
|
|
||||||
multiParam: true
|
|
||||||
acceptableValues: []
|
|
||||||
- parameterName: "--truststore-password"
|
|
||||||
parameterType: "java.lang.String"
|
|
||||||
required: false
|
|
||||||
multiParam: false
|
|
||||||
acceptableValues: []
|
|
||||||
- parameterName: "--truststore-type"
|
|
||||||
parameterType: "java.lang.String"
|
|
||||||
required: false
|
|
||||||
multiParam: false
|
|
||||||
acceptableValues: []
|
|
||||||
- parameterName: "--user"
|
|
||||||
parameterType: "java.lang.String"
|
|
||||||
required: false
|
|
||||||
multiParam: false
|
|
||||||
acceptableValues: []
|
|
||||||
- parameterName: "--verbose"
|
|
||||||
parameterType: "boolean"
|
|
||||||
required: false
|
|
||||||
multiParam: false
|
|
||||||
acceptableValues: []
|
|
||||||
- parameterName: "-a"
|
|
||||||
parameterType: "java.lang.String"
|
|
||||||
required: false
|
|
||||||
multiParam: false
|
|
||||||
acceptableValues: []
|
|
||||||
- parameterName: "-c"
|
|
||||||
parameterType: "java.nio.file.Path"
|
|
||||||
required: false
|
|
||||||
multiParam: true
|
|
||||||
acceptableValues: []
|
|
||||||
- parameterName: "-f"
|
|
||||||
parameterType: "java.nio.file.Path"
|
|
||||||
required: false
|
|
||||||
multiParam: true
|
|
||||||
acceptableValues: []
|
|
||||||
- parameterName: "-o"
|
|
||||||
parameterType: "java.nio.file.Path"
|
|
||||||
required: false
|
|
||||||
multiParam: true
|
|
||||||
acceptableValues: []
|
|
||||||
- parameterName: "-p"
|
|
||||||
parameterType: "java.lang.String"
|
|
||||||
required: false
|
|
||||||
multiParam: false
|
|
||||||
acceptableValues: []
|
|
||||||
- parameterName: "-v"
|
|
||||||
parameterType: "boolean"
|
|
||||||
required: false
|
|
||||||
multiParam: false
|
|
||||||
acceptableValues: []
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
|||||||
apply plugin: 'kotlin'
|
|
||||||
apply plugin: 'java'
|
|
||||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
|
||||||
apply plugin: 'net.corda.plugins.publish-utils'
|
|
||||||
apply plugin: 'com.jfrog.artifactory'
|
|
||||||
|
|
||||||
description 'Corda Shell'
|
|
||||||
|
|
||||||
configurations {
|
|
||||||
integrationTestCompile.extendsFrom testCompile
|
|
||||||
integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
integrationTest {
|
|
||||||
kotlin {
|
|
||||||
compileClasspath += main.output + test.output
|
|
||||||
runtimeClasspath += main.output + test.output
|
|
||||||
srcDir file('src/integration-test/kotlin')
|
|
||||||
}
|
|
||||||
resources {
|
|
||||||
srcDir file('src/integration-test/resources')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
|
||||||
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
|
||||||
|
|
||||||
compile project(':node-api')
|
|
||||||
compile project(':client:rpc')
|
|
||||||
|
|
||||||
// Jackson support: serialisation to/from JSON, YAML, etc.
|
|
||||||
compile project(':client:jackson')
|
|
||||||
|
|
||||||
// CRaSH: An embeddable monitoring and admin shell with support for adding new commands written in Groovy.
|
|
||||||
compile("org.crashub:crash.shell:$crash_version") {
|
|
||||||
exclude group: "org.slf4j", module: "slf4j-jdk14"
|
|
||||||
exclude group: "org.bouncycastle"
|
|
||||||
}
|
|
||||||
|
|
||||||
compile("org.crashub:crash.connectors.ssh:$crash_version") {
|
|
||||||
exclude group: "org.slf4j", module: "slf4j-jdk14"
|
|
||||||
exclude group: "org.bouncycastle"
|
|
||||||
}
|
|
||||||
|
|
||||||
// JAnsi: for drawing things to the terminal in nicely coloured ways.
|
|
||||||
compile "org.fusesource.jansi:jansi:$jansi_version"
|
|
||||||
|
|
||||||
// Manifests: for reading stuff from the manifest file.
|
|
||||||
compile "com.jcabi:jcabi-manifests:1.1"
|
|
||||||
|
|
||||||
// For logging, required for ANSIProgressRenderer.
|
|
||||||
compile "org.apache.logging.log4j:log4j-core:$log4j_version"
|
|
||||||
|
|
||||||
testImplementation "junit:junit:$junit_version"
|
|
||||||
|
|
||||||
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}"
|
|
||||||
testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}"
|
|
||||||
|
|
||||||
// Unit testing helpers.
|
|
||||||
testCompile "org.assertj:assertj-core:$assertj_version"
|
|
||||||
testCompile project(':test-utils')
|
|
||||||
testCompile project(':finance:contracts')
|
|
||||||
testCompile project(':finance:workflows')
|
|
||||||
|
|
||||||
// Jsh: Testing SSH server.
|
|
||||||
integrationTestCompile "com.jcraft:jsch:$jsch_version"
|
|
||||||
|
|
||||||
integrationTestCompile project(':node-driver')
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType(JavaCompile).configureEach {
|
|
||||||
// Resolves a Gradle warning about not scanning for pre-processors.
|
|
||||||
options.compilerArgs << '-proc:none'
|
|
||||||
}
|
|
||||||
|
|
||||||
task integrationTest(type: Test) {
|
|
||||||
testClassesDirs = sourceSets.integrationTest.output.classesDirs
|
|
||||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
|
||||||
}
|
|
||||||
|
|
||||||
jar {
|
|
||||||
baseName 'corda-shell'
|
|
||||||
}
|
|
||||||
|
|
||||||
publish {
|
|
||||||
name jar.baseName
|
|
||||||
}
|
|
@ -1,577 +0,0 @@
|
|||||||
package net.corda.tools.shell
|
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import com.fasterxml.jackson.databind.type.TypeFactory
|
|
||||||
import com.jcraft.jsch.ChannelExec
|
|
||||||
import com.jcraft.jsch.JSch
|
|
||||||
import com.nhaarman.mockito_kotlin.any
|
|
||||||
import com.nhaarman.mockito_kotlin.doAnswer
|
|
||||||
import com.nhaarman.mockito_kotlin.mock
|
|
||||||
import net.corda.client.jackson.JacksonSupport
|
|
||||||
import net.corda.client.jackson.internal.valueAs
|
|
||||||
import net.corda.client.rpc.RPCException
|
|
||||||
import net.corda.core.contracts.*
|
|
||||||
import net.corda.core.crypto.SecureHash
|
|
||||||
import net.corda.core.flows.*
|
|
||||||
import net.corda.core.identity.AbstractParty
|
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.internal.concurrent.transpose
|
|
||||||
import net.corda.core.internal.createDirectories
|
|
||||||
import net.corda.core.internal.div
|
|
||||||
import net.corda.core.internal.inputStream
|
|
||||||
import net.corda.core.internal.list
|
|
||||||
import net.corda.core.messaging.ClientRpcSslOptions
|
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
|
||||||
import net.corda.core.messaging.startFlow
|
|
||||||
import net.corda.core.node.ServiceHub
|
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
|
||||||
import net.corda.core.transactions.SignedTransaction
|
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
|
||||||
import net.corda.core.utilities.ProgressTracker
|
|
||||||
import net.corda.core.utilities.getOrThrow
|
|
||||||
import net.corda.core.utilities.seconds
|
|
||||||
import net.corda.node.internal.NodeStartup
|
|
||||||
import net.corda.node.services.Permissions
|
|
||||||
import net.corda.node.services.Permissions.Companion.all
|
|
||||||
import net.corda.node.services.Permissions.Companion.startFlow
|
|
||||||
import net.corda.node.services.config.shell.toShellConfig
|
|
||||||
import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate
|
|
||||||
import net.corda.node.utilities.saveToKeyStore
|
|
||||||
import net.corda.node.utilities.saveToTrustStore
|
|
||||||
import net.corda.nodeapi.BrokerRpcSslOptions
|
|
||||||
import net.corda.testing.core.ALICE_NAME
|
|
||||||
import net.corda.testing.core.BOB_NAME
|
|
||||||
import net.corda.testing.core.singleIdentity
|
|
||||||
import net.corda.testing.driver.DriverParameters
|
|
||||||
import net.corda.testing.driver.NodeHandle
|
|
||||||
import net.corda.testing.driver.driver
|
|
||||||
import net.corda.testing.driver.internal.NodeHandleInternal
|
|
||||||
import net.corda.testing.driver.internal.checkpoint.CheckpointRpcHelper.checkpointsRpc
|
|
||||||
import net.corda.testing.internal.useSslRpcOverrides
|
|
||||||
import net.corda.testing.node.User
|
|
||||||
import net.corda.testing.node.internal.enclosedCordapp
|
|
||||||
import net.corda.tools.shell.SSHServerTest.FlowICanRun
|
|
||||||
import net.corda.tools.shell.utlities.ANSIProgressRenderer
|
|
||||||
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
|
||||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
|
||||||
import org.bouncycastle.util.io.Streams
|
|
||||||
import org.crsh.text.RenderPrintWriter
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Ignore
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.jupiter.api.assertThrows
|
|
||||||
import org.junit.rules.TemporaryFolder
|
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.CompletableFuture
|
|
||||||
import java.util.concurrent.Semaphore
|
|
||||||
import java.util.concurrent.TimeoutException
|
|
||||||
import java.util.zip.ZipInputStream
|
|
||||||
import javax.security.auth.x500.X500Principal
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertNotNull
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
class InteractiveShellIntegrationTest {
|
|
||||||
@Rule
|
|
||||||
@JvmField
|
|
||||||
val tempFolder = TemporaryFolder()
|
|
||||||
|
|
||||||
private val testName = X500Principal("CN=Test,O=R3 Ltd,L=London,C=GB")
|
|
||||||
|
|
||||||
private lateinit var inputObjectMapper: ObjectMapper
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
inputObjectMapper = objectMapperWithClassLoader(InteractiveShell.getCordappsClassloader())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `shell should not log in with invalid credentials`() {
|
|
||||||
val user = User("u", "p", setOf())
|
|
||||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList())) {
|
|
||||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
|
||||||
startShell("fake", "fake", node.rpcAddress)
|
|
||||||
assertThatThrownBy { InteractiveShell.nodeInfo() }.isInstanceOf(ActiveMQSecurityException::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `shell should log in with valid credentials`() {
|
|
||||||
val user = User("u", "p", setOf())
|
|
||||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList())) {
|
|
||||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
|
||||||
startShell(node)
|
|
||||||
InteractiveShell.nodeInfo()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `shell should log in with ssl`() {
|
|
||||||
val user = User("mark", "dadada", setOf(all()))
|
|
||||||
|
|
||||||
val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(testName)
|
|
||||||
val keyStorePath = saveToKeyStore(tempFolder.root.toPath() / "keystore.jks", keyPair, cert)
|
|
||||||
val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password")
|
|
||||||
|
|
||||||
val trustStorePath = saveToTrustStore(tempFolder.root.toPath() / "truststore.jks", cert)
|
|
||||||
val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password")
|
|
||||||
|
|
||||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList())) {
|
|
||||||
val node = startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow()
|
|
||||||
startShell(node, clientSslOptions)
|
|
||||||
InteractiveShell.nodeInfo()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `shell shoud not log in with invalid truststore`() {
|
|
||||||
val user = User("mark", "dadada", setOf("ALL"))
|
|
||||||
val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(testName)
|
|
||||||
val keyStorePath = saveToKeyStore(tempFolder.root.toPath() / "keystore.jks", keyPair, cert)
|
|
||||||
val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password")
|
|
||||||
|
|
||||||
val (_, cert1) = createKeyPairAndSelfSignedTLSCertificate(testName)
|
|
||||||
val trustStorePath = saveToTrustStore(tempFolder.root.toPath() / "truststore.jks", cert1)
|
|
||||||
val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password")
|
|
||||||
|
|
||||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList())) {
|
|
||||||
val node = startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow()
|
|
||||||
startShell(node, clientSslOptions)
|
|
||||||
assertThatThrownBy { InteractiveShell.nodeInfo() }.isInstanceOf(RPCException::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `internal shell user should not be able to connect if node started with devMode=false`() {
|
|
||||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = emptyList())) {
|
|
||||||
val node = startNode().getOrThrow()
|
|
||||||
val conf = (node as NodeHandleInternal).configuration.toShellConfig()
|
|
||||||
InteractiveShell.startShell(conf)
|
|
||||||
assertThatThrownBy { InteractiveShell.nodeInfo() }.isInstanceOf(ActiveMQSecurityException::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `ssh runs flows via standalone shell`() {
|
|
||||||
val user = User("u", "p", setOf(
|
|
||||||
startFlow<FlowICanRun>(),
|
|
||||||
Permissions.invokeRpc(CordaRPCOps::registeredFlows),
|
|
||||||
Permissions.invokeRpc(CordaRPCOps::nodeInfo)
|
|
||||||
))
|
|
||||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) {
|
|
||||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
|
||||||
startShell(node, sshdPort = 2224)
|
|
||||||
InteractiveShell.nodeInfo()
|
|
||||||
|
|
||||||
val session = JSch().getSession("u", "localhost", 2224)
|
|
||||||
session.setConfig("StrictHostKeyChecking", "no")
|
|
||||||
session.setPassword("p")
|
|
||||||
session.connect()
|
|
||||||
|
|
||||||
assertTrue(session.isConnected)
|
|
||||||
|
|
||||||
val channel = session.openChannel("exec") as ChannelExec
|
|
||||||
channel.setCommand("start FlowICanRun")
|
|
||||||
channel.connect(5000)
|
|
||||||
|
|
||||||
assertTrue(channel.isConnected)
|
|
||||||
|
|
||||||
val response = String(Streams.readAll(channel.inputStream))
|
|
||||||
|
|
||||||
val linesWithDoneCount = response.lines().filter { line -> "Done" in line }
|
|
||||||
|
|
||||||
channel.disconnect()
|
|
||||||
session.disconnect()
|
|
||||||
|
|
||||||
// There are ANSI control characters involved, so we want to avoid direct byte to byte matching.
|
|
||||||
assertThat(linesWithDoneCount).hasSize(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `ssh run flows via standalone shell over ssl to node`() {
|
|
||||||
val user = User("mark", "dadada", setOf(
|
|
||||||
startFlow<FlowICanRun>(),
|
|
||||||
Permissions.invokeRpc(CordaRPCOps::registeredFlows),
|
|
||||||
Permissions.invokeRpc(CordaRPCOps::nodeInfo)/*all()*/
|
|
||||||
))
|
|
||||||
|
|
||||||
val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(testName)
|
|
||||||
val keyStorePath = saveToKeyStore(tempFolder.root.toPath() / "keystore.jks", keyPair, cert)
|
|
||||||
val brokerSslOptions = BrokerRpcSslOptions(keyStorePath, "password")
|
|
||||||
val trustStorePath = saveToTrustStore(tempFolder.root.toPath() / "truststore.jks", cert)
|
|
||||||
val clientSslOptions = ClientRpcSslOptions(trustStorePath, "password")
|
|
||||||
|
|
||||||
var successful = false
|
|
||||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) {
|
|
||||||
startNode(rpcUsers = listOf(user), customOverrides = brokerSslOptions.useSslRpcOverrides()).getOrThrow().use { node ->
|
|
||||||
startShell(node, clientSslOptions, sshdPort = 2223)
|
|
||||||
InteractiveShell.nodeInfo()
|
|
||||||
|
|
||||||
val session = JSch().getSession("mark", "localhost", 2223)
|
|
||||||
session.setConfig("StrictHostKeyChecking", "no")
|
|
||||||
session.setPassword("dadada")
|
|
||||||
session.connect()
|
|
||||||
|
|
||||||
assertTrue(session.isConnected)
|
|
||||||
|
|
||||||
val channel = session.openChannel("exec") as ChannelExec
|
|
||||||
channel.setCommand("start FlowICanRun")
|
|
||||||
channel.connect(5000)
|
|
||||||
|
|
||||||
assertTrue(channel.isConnected)
|
|
||||||
|
|
||||||
val response = String(Streams.readAll(channel.inputStream))
|
|
||||||
|
|
||||||
val linesWithDoneCount = response.lines().filter { line -> "Done" in line }
|
|
||||||
|
|
||||||
channel.disconnect()
|
|
||||||
session.disconnect() // TODO Simon make sure to close them
|
|
||||||
|
|
||||||
// There are ANSI control characters involved, so we want to avoid direct byte to byte matching.
|
|
||||||
assertThat(linesWithDoneCount).hasSize(1)
|
|
||||||
|
|
||||||
successful = true
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThat(successful).isTrue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `shell should start flow with fully qualified class name`() {
|
|
||||||
val user = User("u", "p", setOf(all()))
|
|
||||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) {
|
|
||||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
|
||||||
startShell(node)
|
|
||||||
val (output, lines) = mockRenderPrintWriter()
|
|
||||||
InteractiveShell.runFlowByNameFragment(NoOpFlow::class.java.name, "", output, node.rpc, mockAnsiProgressRenderer())
|
|
||||||
assertThat(lines.last()).startsWith("Flow completed with result:")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `shell should start flow with unique un-qualified class name`() {
|
|
||||||
val user = User("u", "p", setOf(all()))
|
|
||||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) {
|
|
||||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
|
||||||
startShell(node)
|
|
||||||
val (output, lines) = mockRenderPrintWriter()
|
|
||||||
InteractiveShell.runFlowByNameFragment("NoOpFlowA", "", output, node.rpc, mockAnsiProgressRenderer())
|
|
||||||
assertThat(lines.last()).startsWith("Flow completed with result:")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `shell should fail to start flow with ambiguous class name`() {
|
|
||||||
val user = User("u", "p", setOf(all()))
|
|
||||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) {
|
|
||||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
|
||||||
startShell(node)
|
|
||||||
val (output, lines) = mockRenderPrintWriter()
|
|
||||||
InteractiveShell.runFlowByNameFragment("NoOpFlo", "", output, node.rpc, mockAnsiProgressRenderer())
|
|
||||||
assertThat(lines.any { it.startsWith("Ambiguous name provided, please be more specific.") }).isTrue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `shell should start flow with partially matching class name`() {
|
|
||||||
val user = User("u", "p", setOf(all()))
|
|
||||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) {
|
|
||||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
|
||||||
startShell(node)
|
|
||||||
val (output, lines) = mockRenderPrintWriter()
|
|
||||||
InteractiveShell.runFlowByNameFragment("Burble", "", output, node.rpc, mockAnsiProgressRenderer())
|
|
||||||
assertThat(lines.last()).startsWith("Flow completed with result")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `dumpCheckpoints correctly serializes FlowExternalOperations`() {
|
|
||||||
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
|
|
||||||
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
|
|
||||||
(alice.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).createDirectories()
|
|
||||||
alice.rpc.startFlow(::ExternalOperationFlow)
|
|
||||||
ExternalOperation.lock.acquire()
|
|
||||||
alice.checkpointsRpc.use { InteractiveShell.runDumpCheckpoints(it) }
|
|
||||||
ExternalOperation.lock2.release()
|
|
||||||
|
|
||||||
val zipFile = (alice.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).list().first { "checkpoints_dump-" in it.toString() }
|
|
||||||
val json = ZipInputStream(zipFile.inputStream()).use { zip ->
|
|
||||||
zip.nextEntry
|
|
||||||
ObjectMapper().readTree(zip)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertEquals("hello there", json["suspendedOn"]["customOperation"]["operation"]["a"].asText())
|
|
||||||
assertEquals(123, json["suspendedOn"]["customOperation"]["operation"]["b"].asInt())
|
|
||||||
assertEquals("please work", json["suspendedOn"]["customOperation"]["operation"]["c"]["d"].asText())
|
|
||||||
assertEquals("I beg you", json["suspendedOn"]["customOperation"]["operation"]["c"]["e"].asText())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `dumpCheckpoints correctly serializes FlowExternalAsyncOperations`() {
|
|
||||||
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
|
|
||||||
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
|
|
||||||
(alice.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).createDirectories()
|
|
||||||
alice.rpc.startFlow(::ExternalAsyncOperationFlow)
|
|
||||||
ExternalAsyncOperation.lock.acquire()
|
|
||||||
alice.checkpointsRpc.use { InteractiveShell.runDumpCheckpoints(it) }
|
|
||||||
ExternalAsyncOperation.future.complete(null)
|
|
||||||
val zipFile = (alice.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).list().first { "checkpoints_dump-" in it.toString() }
|
|
||||||
val json = ZipInputStream(zipFile.inputStream()).use { zip ->
|
|
||||||
zip.nextEntry
|
|
||||||
ObjectMapper().readTree(zip)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertEquals("hello there", json["suspendedOn"]["customOperation"]["operation"]["a"].asText())
|
|
||||||
assertEquals(123, json["suspendedOn"]["customOperation"]["operation"]["b"].asInt())
|
|
||||||
assertEquals("please work", json["suspendedOn"]["customOperation"]["operation"]["c"]["d"].asText())
|
|
||||||
assertEquals("I beg you", json["suspendedOn"]["customOperation"]["operation"]["c"]["e"].asText())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `dumpCheckpoints correctly serializes WaitForStateConsumption`() {
|
|
||||||
driver(DriverParameters(notarySpecs = emptyList(), startNodesInProcess = true)) {
|
|
||||||
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
|
|
||||||
(alice.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).createDirectories()
|
|
||||||
val stateRefs = setOf(
|
|
||||||
StateRef(SecureHash.randomSHA256(), 0),
|
|
||||||
StateRef(SecureHash.randomSHA256(), 1),
|
|
||||||
StateRef(SecureHash.randomSHA256(), 2)
|
|
||||||
)
|
|
||||||
assertThrows<TimeoutException> {
|
|
||||||
alice.rpc.startFlow(::WaitForStateConsumptionFlow, stateRefs).returnValue.getOrThrow(10.seconds)
|
|
||||||
}
|
|
||||||
alice.checkpointsRpc.use { InteractiveShell.runDumpCheckpoints(it) }
|
|
||||||
val zipFile = (alice.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).list().first { "checkpoints_dump-" in it.toString() }
|
|
||||||
val json = ZipInputStream(zipFile.inputStream()).use { zip ->
|
|
||||||
zip.nextEntry
|
|
||||||
ObjectMapper().readTree(zip)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertEquals(stateRefs, json["suspendedOn"]["waitForStateConsumption"].valueAs<List<StateRef>>(inputObjectMapper).toSet())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `dumpCheckpoints creates zip with json file for suspended flow`() {
|
|
||||||
val user = User("u", "p", setOf(all()))
|
|
||||||
driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = listOf(enclosedCordapp()))) {
|
|
||||||
val (aliceNode, bobNode) = listOf(ALICE_NAME, BOB_NAME)
|
|
||||||
.map { startNode(providedName = it, rpcUsers = listOf(user)) }
|
|
||||||
.transpose()
|
|
||||||
.getOrThrow()
|
|
||||||
bobNode.stop()
|
|
||||||
|
|
||||||
// Create logs directory since the driver is not creating it
|
|
||||||
(aliceNode.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).createDirectories()
|
|
||||||
|
|
||||||
startShell(aliceNode)
|
|
||||||
|
|
||||||
val linearId = UniqueIdentifier(id = UUID.fromString("7c0719f0-e489-46e8-bf3b-ee203156fc7c"))
|
|
||||||
aliceNode.rpc.startFlow(
|
|
||||||
::FlowForCheckpointDumping,
|
|
||||||
MyState(
|
|
||||||
"some random string",
|
|
||||||
linearId,
|
|
||||||
listOf(aliceNode.nodeInfo.singleIdentity(), bobNode.nodeInfo.singleIdentity())
|
|
||||||
),
|
|
||||||
bobNode.nodeInfo.singleIdentity()
|
|
||||||
)
|
|
||||||
|
|
||||||
Thread.sleep(5000)
|
|
||||||
|
|
||||||
mockRenderPrintWriter()
|
|
||||||
aliceNode.checkpointsRpc.use { InteractiveShell.runDumpCheckpoints(it) }
|
|
||||||
|
|
||||||
val zipFile = (aliceNode.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).list().first { "checkpoints_dump-" in it.toString() }
|
|
||||||
val json = ZipInputStream(zipFile.inputStream()).use { zip ->
|
|
||||||
zip.nextEntry
|
|
||||||
ObjectMapper().readTree(zip)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertNotNull(json["flowId"].asText())
|
|
||||||
assertEquals(FlowForCheckpointDumping::class.java.name, json["topLevelFlowClass"].asText())
|
|
||||||
assertEquals(linearId.id.toString(), json["topLevelFlowLogic"]["myState"]["linearId"]["id"].asText())
|
|
||||||
assertEquals(4, json["flowCallStackSummary"].size())
|
|
||||||
assertEquals(4, json["flowCallStack"].size())
|
|
||||||
val sendAndReceiveJson = json["suspendedOn"]["sendAndReceive"][0]
|
|
||||||
assertEquals(bobNode.nodeInfo.singleIdentity().toString(), sendAndReceiveJson["session"]["peer"].asText())
|
|
||||||
assertEquals(SignedTransaction::class.qualifiedName, sendAndReceiveJson["sentPayloadType"].asText())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startShell(node: NodeHandle, ssl: ClientRpcSslOptions? = null, sshdPort: Int? = null) {
|
|
||||||
val user = node.rpcUsers[0]
|
|
||||||
startShell(user.username, user.password, node.rpcAddress, ssl, sshdPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startShell(user: String, password: String, address: NetworkHostAndPort, ssl: ClientRpcSslOptions? = null, sshdPort: Int? = null) {
|
|
||||||
val conf = ShellConfiguration(
|
|
||||||
commandsDirectory = tempFolder.newFolder().toPath(),
|
|
||||||
user = user,
|
|
||||||
password = password,
|
|
||||||
hostAndPort = address,
|
|
||||||
ssl = ssl,
|
|
||||||
sshdPort = sshdPort
|
|
||||||
)
|
|
||||||
InteractiveShell.startShell(conf)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mockRenderPrintWriter(): Pair<RenderPrintWriter, List<String>> {
|
|
||||||
val lines = ArrayList<String>()
|
|
||||||
val writer = mock<RenderPrintWriter> {
|
|
||||||
on { println(any<String>()) } doAnswer {
|
|
||||||
val line = it.getArgument(0, String::class.java)
|
|
||||||
println(">>> $line")
|
|
||||||
lines += line
|
|
||||||
Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Pair(writer, lines)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mockAnsiProgressRenderer(): ANSIProgressRenderer {
|
|
||||||
return mock {
|
|
||||||
on { render(any(), any()) } doAnswer { InteractiveShell.latch.countDown() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun objectMapperWithClassLoader(classLoader: ClassLoader?): ObjectMapper {
|
|
||||||
val objectMapper = JacksonSupport.createNonRpcMapper()
|
|
||||||
val tf = TypeFactory.defaultInstance().withClassLoader(classLoader)
|
|
||||||
objectMapper.typeFactory = tf
|
|
||||||
return objectMapper
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNUSED")
|
|
||||||
@StartableByRPC
|
|
||||||
class NoOpFlow : FlowLogic<Unit>() {
|
|
||||||
override val progressTracker = ProgressTracker()
|
|
||||||
override fun call() {
|
|
||||||
println("NO OP!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNUSED")
|
|
||||||
@StartableByRPC
|
|
||||||
class NoOpFlowA : FlowLogic<Unit>() {
|
|
||||||
override val progressTracker = ProgressTracker()
|
|
||||||
override fun call() {
|
|
||||||
println("NO OP! (A)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNUSED")
|
|
||||||
@StartableByRPC
|
|
||||||
class BurbleFlow : FlowLogic<Unit>() {
|
|
||||||
override val progressTracker = ProgressTracker()
|
|
||||||
override fun call() {
|
|
||||||
println("NO OP! (Burble)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@InitiatingFlow
|
|
||||||
@StartableByRPC
|
|
||||||
class FlowForCheckpointDumping(private val myState: MyState, private val party: Party): FlowLogic<Unit>() {
|
|
||||||
// Make sure any SerializeAsToken instances are not serialised
|
|
||||||
private var services: ServiceHub? = null
|
|
||||||
|
|
||||||
@Suspendable
|
|
||||||
override fun call() {
|
|
||||||
services = serviceHub
|
|
||||||
val tx = TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first()).apply {
|
|
||||||
addOutputState(myState)
|
|
||||||
addCommand(MyContract.Create(), listOf(ourIdentity, party).map(Party::owningKey))
|
|
||||||
}
|
|
||||||
val sessions = listOf(initiateFlow(party))
|
|
||||||
val stx = serviceHub.signInitialTransaction(tx)
|
|
||||||
subFlow(CollectSignaturesFlow(stx, sessions))
|
|
||||||
throw IllegalStateException("The test should not get here")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@InitiatedBy(FlowForCheckpointDumping::class)
|
|
||||||
class FlowForCheckpointDumpingResponder(private val session: FlowSession): FlowLogic<Unit>() {
|
|
||||||
override fun call() {
|
|
||||||
val signTxFlow = object : SignTransactionFlow(session) {
|
|
||||||
override fun checkTransaction(stx: SignedTransaction) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
subFlow(signTxFlow)
|
|
||||||
throw IllegalStateException("The test should not get here")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MyContract : Contract {
|
|
||||||
class Create : CommandData
|
|
||||||
override fun verify(tx: LedgerTransaction) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
@BelongsToContract(MyContract::class)
|
|
||||||
data class MyState(
|
|
||||||
val data: String,
|
|
||||||
override val linearId: UniqueIdentifier,
|
|
||||||
override val participants: List<AbstractParty>
|
|
||||||
) : LinearState
|
|
||||||
|
|
||||||
@StartableByRPC
|
|
||||||
class ExternalAsyncOperationFlow : FlowLogic<Unit>() {
|
|
||||||
@Suspendable
|
|
||||||
override fun call() {
|
|
||||||
await(ExternalAsyncOperation("hello there", 123, Data("please work", "I beg you")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ExternalAsyncOperation(val a: String, val b: Int, val c: Data): FlowExternalAsyncOperation<Unit> {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val future = CompletableFuture<Unit>()
|
|
||||||
val lock = Semaphore(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun execute(deduplicationId: String): CompletableFuture<Unit> {
|
|
||||||
return future.also { lock.release() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Data(val d: String, val e: String)
|
|
||||||
|
|
||||||
@StartableByRPC
|
|
||||||
class ExternalOperationFlow : FlowLogic<Unit>() {
|
|
||||||
@Suspendable
|
|
||||||
override fun call() {
|
|
||||||
await(ExternalOperation("hello there", 123, Data("please work", "I beg you")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ExternalOperation(val a: String, val b: Int, val c: Data): FlowExternalOperation<Unit> {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val lock = Semaphore(0)
|
|
||||||
val lock2 = Semaphore(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun execute(deduplicationId: String) {
|
|
||||||
lock.release()
|
|
||||||
lock2.acquire()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@StartableByRPC
|
|
||||||
class WaitForStateConsumptionFlow(private val stateRefs: Set<StateRef>) : FlowLogic<Unit>() {
|
|
||||||
@Suspendable
|
|
||||||
override fun call() {
|
|
||||||
waitForStateConsumption(stateRefs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,184 +0,0 @@
|
|||||||
package net.corda.tools.shell
|
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
|
||||||
import com.jcraft.jsch.ChannelExec
|
|
||||||
import com.jcraft.jsch.JSch
|
|
||||||
import com.jcraft.jsch.JSchException
|
|
||||||
import net.corda.core.flows.FlowLogic
|
|
||||||
import net.corda.core.flows.InitiatingFlow
|
|
||||||
import net.corda.core.flows.StartableByRPC
|
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
|
||||||
import net.corda.core.utilities.ProgressTracker
|
|
||||||
import net.corda.core.utilities.getOrThrow
|
|
||||||
import net.corda.core.utilities.unwrap
|
|
||||||
import net.corda.node.services.Permissions.Companion.invokeRpc
|
|
||||||
import net.corda.node.services.Permissions.Companion.startFlow
|
|
||||||
import net.corda.testing.core.ALICE_NAME
|
|
||||||
import net.corda.testing.driver.DriverParameters
|
|
||||||
import net.corda.testing.driver.driver
|
|
||||||
import net.corda.testing.node.User
|
|
||||||
import net.corda.testing.node.internal.enclosedCordapp
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
|
||||||
import org.bouncycastle.util.io.Streams
|
|
||||||
import org.junit.Ignore
|
|
||||||
import org.junit.Test
|
|
||||||
import java.net.ConnectException
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
import kotlin.test.fail
|
|
||||||
|
|
||||||
class SSHServerTest {
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `ssh server does not start by default`() {
|
|
||||||
val user = User("u", "p", setOf())
|
|
||||||
// The driver will automatically pick up the annotated flows below
|
|
||||||
driver(DriverParameters(notarySpecs = emptyList(), cordappsForAllNodes = emptyList())) {
|
|
||||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user))
|
|
||||||
node.getOrThrow()
|
|
||||||
|
|
||||||
val session = JSch().getSession("u", "localhost", 2222)
|
|
||||||
session.setConfig("StrictHostKeyChecking", "no")
|
|
||||||
session.setPassword("p")
|
|
||||||
|
|
||||||
try {
|
|
||||||
session.connect()
|
|
||||||
fail()
|
|
||||||
} catch (e: JSchException) {
|
|
||||||
assertTrue(e.cause is ConnectException)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `ssh server starts when configured`() {
|
|
||||||
val user = User("u", "p", setOf())
|
|
||||||
// The driver will automatically pick up the annotated flows below
|
|
||||||
driver(DriverParameters(notarySpecs = emptyList(), cordappsForAllNodes = emptyList())) {
|
|
||||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user),
|
|
||||||
customOverrides = mapOf("sshd" to mapOf("port" to 2222)) /*, startInSameProcess = true */)
|
|
||||||
node.getOrThrow()
|
|
||||||
|
|
||||||
val session = JSch().getSession("u", "localhost", 2222)
|
|
||||||
session.setConfig("StrictHostKeyChecking", "no")
|
|
||||||
session.setPassword("p")
|
|
||||||
|
|
||||||
session.connect()
|
|
||||||
|
|
||||||
assertTrue(session.isConnected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `ssh server verify credentials`() {
|
|
||||||
val user = User("u", "p", setOf())
|
|
||||||
// The driver will automatically pick up the annotated flows below
|
|
||||||
driver(DriverParameters(notarySpecs = emptyList(), cordappsForAllNodes = emptyList())) {
|
|
||||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user),
|
|
||||||
customOverrides = mapOf("sshd" to mapOf("port" to 2222)))
|
|
||||||
node.getOrThrow()
|
|
||||||
|
|
||||||
val session = JSch().getSession("u", "localhost", 2222)
|
|
||||||
session.setConfig("StrictHostKeyChecking", "no")
|
|
||||||
session.setPassword("p_is_bad_password")
|
|
||||||
|
|
||||||
try {
|
|
||||||
session.connect()
|
|
||||||
fail("Server should reject invalid credentials")
|
|
||||||
} catch (e: JSchException) {
|
|
||||||
//There is no specialized exception for this
|
|
||||||
assertTrue(e.message == "Auth fail")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `ssh respects permissions`() {
|
|
||||||
val user = User("u", "p", setOf(startFlow<FlowICanRun>(),
|
|
||||||
invokeRpc(CordaRPCOps::wellKnownPartyFromX500Name)))
|
|
||||||
// The driver will automatically pick up the annotated flows below
|
|
||||||
driver(DriverParameters(notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) {
|
|
||||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user),
|
|
||||||
customOverrides = mapOf("sshd" to mapOf("port" to 2222)))
|
|
||||||
node.getOrThrow()
|
|
||||||
|
|
||||||
val session = JSch().getSession("u", "localhost", 2222)
|
|
||||||
session.setConfig("StrictHostKeyChecking", "no")
|
|
||||||
session.setPassword("p")
|
|
||||||
session.connect()
|
|
||||||
|
|
||||||
assertTrue(session.isConnected)
|
|
||||||
|
|
||||||
val channel = session.openChannel("exec") as ChannelExec
|
|
||||||
channel.setCommand("start FlowICannotRun otherParty: \"$ALICE_NAME\"")
|
|
||||||
channel.connect()
|
|
||||||
val response = String(Streams.readAll(channel.inputStream))
|
|
||||||
|
|
||||||
channel.disconnect()
|
|
||||||
session.disconnect()
|
|
||||||
|
|
||||||
assertThat(response).matches("(?s)User not authorized to perform RPC call .*")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Ignore
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `ssh runs flows`() {
|
|
||||||
val user = User("u", "p", setOf(startFlow<FlowICanRun>()))
|
|
||||||
// The driver will automatically pick up the annotated flows below
|
|
||||||
driver(DriverParameters(notarySpecs = emptyList(), cordappsForAllNodes = listOf(enclosedCordapp()))) {
|
|
||||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user),
|
|
||||||
customOverrides = mapOf("sshd" to mapOf("port" to 2222)))
|
|
||||||
node.getOrThrow()
|
|
||||||
|
|
||||||
val session = JSch().getSession("u", "localhost", 2222)
|
|
||||||
session.setConfig("StrictHostKeyChecking", "no")
|
|
||||||
session.setPassword("p")
|
|
||||||
session.connect()
|
|
||||||
|
|
||||||
assertTrue(session.isConnected)
|
|
||||||
|
|
||||||
val channel = session.openChannel("exec") as ChannelExec
|
|
||||||
channel.setCommand("start FlowICanRun")
|
|
||||||
channel.connect(5000)
|
|
||||||
|
|
||||||
assertTrue(channel.isConnected)
|
|
||||||
|
|
||||||
val response = String(Streams.readAll(channel.inputStream))
|
|
||||||
|
|
||||||
val linesWithDoneCount = response.lines().filter { line -> line.contains("Done") }
|
|
||||||
|
|
||||||
channel.disconnect()
|
|
||||||
session.disconnect()
|
|
||||||
|
|
||||||
// There are ANSI control characters involved, so we want to avoid direct byte to byte matching.
|
|
||||||
assertThat(linesWithDoneCount).size().isGreaterThanOrEqualTo(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@StartableByRPC
|
|
||||||
@InitiatingFlow
|
|
||||||
class FlowICanRun : FlowLogic<String>() {
|
|
||||||
|
|
||||||
private val HELLO_STEP = ProgressTracker.Step("Hello")
|
|
||||||
|
|
||||||
@Suspendable
|
|
||||||
override fun call(): String {
|
|
||||||
progressTracker?.currentStep = HELLO_STEP
|
|
||||||
return "bambam"
|
|
||||||
}
|
|
||||||
|
|
||||||
override val progressTracker: ProgressTracker? = ProgressTracker(HELLO_STEP)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
@StartableByRPC
|
|
||||||
@InitiatingFlow
|
|
||||||
class FlowICannotRun(private val otherParty: Party) : FlowLogic<String>() {
|
|
||||||
@Suspendable
|
|
||||||
override fun call(): String = initiateFlow(otherParty).receive<String>().unwrap { it }
|
|
||||||
|
|
||||||
override val progressTracker: ProgressTracker? = ProgressTracker()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
user=demo1
|
|
||||||
baseDirectory="/Users/szymonsztuka/Documents/shell-config"
|
|
||||||
hostAndPort="localhost:10006"
|
|
||||||
sshdPort=2223
|
|
||||||
ssl {
|
|
||||||
keyStorePassword=password
|
|
||||||
trustStorePassword=password
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
package net.corda.tools.shell;
|
|
||||||
|
|
||||||
import net.corda.core.internal.messaging.AttachmentTrustInfoRPCOps;
|
|
||||||
import org.crsh.cli.Command;
|
|
||||||
import org.crsh.cli.Man;
|
|
||||||
import org.crsh.cli.Named;
|
|
||||||
import org.crsh.cli.Usage;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import static net.corda.tools.shell.InteractiveShell.runAttachmentTrustInfoView;
|
|
||||||
|
|
||||||
@Named("attachments")
|
|
||||||
public class AttachmentShellCommand extends InteractiveShellCommand<AttachmentTrustInfoRPCOps> {
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
public Class<AttachmentTrustInfoRPCOps> getRpcOpsClass() {
|
|
||||||
return AttachmentTrustInfoRPCOps.class;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command
|
|
||||||
@Man("Displays the trusted CorDapp attachments that have been manually installed or received over the network")
|
|
||||||
@Usage("Displays the trusted CorDapp attachments that have been manually installed or received over the network")
|
|
||||||
public void trustInfo() {
|
|
||||||
runAttachmentTrustInfoView(out, ops());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
package net.corda.tools.shell;
|
|
||||||
|
|
||||||
import net.corda.core.messaging.flows.FlowManagerRPCOps;
|
|
||||||
import org.crsh.cli.Command;
|
|
||||||
import org.crsh.cli.Man;
|
|
||||||
import org.crsh.cli.Named;
|
|
||||||
import org.crsh.cli.Usage;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import static net.corda.tools.shell.InteractiveShell.*;
|
|
||||||
|
|
||||||
@Named("checkpoints")
|
|
||||||
public class CheckpointShellCommand extends InteractiveShellCommand<FlowManagerRPCOps> {
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
public Class<FlowManagerRPCOps> getRpcOpsClass() {
|
|
||||||
return FlowManagerRPCOps.class;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command
|
|
||||||
@Man("Outputs the contents of all checkpoints as json to be manually reviewed")
|
|
||||||
@Usage("Outputs the contents of all checkpoints as json to be manually reviewed")
|
|
||||||
public void dump() {
|
|
||||||
runDumpCheckpoints(ops());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command
|
|
||||||
@Man("Outputs the contents of all started flow checkpoints in a zip file")
|
|
||||||
@Usage("Outputs the contents of all started flow checkpoints in a zip file")
|
|
||||||
public void debug() {
|
|
||||||
runDebugCheckpoints(ops());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,87 +0,0 @@
|
|||||||
package net.corda.tools.shell;
|
|
||||||
|
|
||||||
// See the comments at the top of run.java
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import net.corda.core.messaging.CordaRPCOps;
|
|
||||||
import net.corda.tools.shell.utlities.ANSIProgressRenderer;
|
|
||||||
import net.corda.tools.shell.utlities.CRaSHANSIProgressRenderer;
|
|
||||||
import org.crsh.cli.*;
|
|
||||||
import org.crsh.command.*;
|
|
||||||
import org.crsh.text.*;
|
|
||||||
import org.crsh.text.ui.TableElement;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import static net.corda.tools.shell.InteractiveShell.killFlowById;
|
|
||||||
import static net.corda.tools.shell.InteractiveShell.runFlowByNameFragment;
|
|
||||||
import static net.corda.tools.shell.InteractiveShell.runStateMachinesView;
|
|
||||||
|
|
||||||
@Man(
|
|
||||||
"Allows you to start and kill flows, list the ones available and to watch flows currently running on the node.\n\n" +
|
|
||||||
"Starting flow is the primary way in which you command the node to change the ledger.\n\n" +
|
|
||||||
"This command is generic, so the right way to use it depends on the flow you wish to start. You can use the 'flow start'\n" +
|
|
||||||
"command with either a full class name, or a substring of the class name that's unambiguous. The parameters to the \n" +
|
|
||||||
"flow constructors (the right one is picked automatically) are then specified using the same syntax as for the run command."
|
|
||||||
)
|
|
||||||
@Named("flow")
|
|
||||||
public class FlowShellCommand extends CordaRpcOpsShellCommand {
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(FlowShellCommand.class);
|
|
||||||
|
|
||||||
@Command
|
|
||||||
@Usage("Start a (work)flow on the node. This is how you can change the ledger.\n\n" +
|
|
||||||
"\t\t Starting flow is the primary way in which you command the node to change the ledger.\n" +
|
|
||||||
"\t\t This command is generic, so the right way to use it depends on the flow you wish to start. You can use the 'flow start'\n" +
|
|
||||||
"\t\t command with either a full class name, or a substring of the class name that's unambiguous. The parameters to the\n" +
|
|
||||||
"\t\t flow constructors (the right one is picked automatically) are then specified using the same syntax as for the run command.\n")
|
|
||||||
public void start(
|
|
||||||
@Usage("The class name of the flow to run, or an unambiguous substring") @Argument String name,
|
|
||||||
@Usage("The data to pass as input") @Argument(unquote = false) List<String> input
|
|
||||||
) {
|
|
||||||
logger.info("Executing command \"flow start {} {}\",", name, (input != null) ? String.join(" ", input) : "<no arguments>");
|
|
||||||
startFlow(name, input, out, ops(), ansiProgressRenderer(), objectMapper(null));
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Limit number of flows shown option?
|
|
||||||
@Command
|
|
||||||
@Usage("Watch information about state machines running on the node with result information.")
|
|
||||||
public void watch(InvocationContext<TableElement> context) throws Exception {
|
|
||||||
logger.info("Executing command \"flow watch\".");
|
|
||||||
runStateMachinesView(out, ops());
|
|
||||||
}
|
|
||||||
|
|
||||||
static void startFlow(@Usage("The class name of the flow to run, or an unambiguous substring") @Argument String name,
|
|
||||||
@Usage("The data to pass as input") @Argument(unquote = false) List<String> input,
|
|
||||||
RenderPrintWriter out,
|
|
||||||
CordaRPCOps rpcOps,
|
|
||||||
ANSIProgressRenderer ansiProgressRenderer,
|
|
||||||
ObjectMapper om) {
|
|
||||||
if (name == null) {
|
|
||||||
out.println("You must pass a name for the flow. Example: \"start Yo target: Some other company\"", Decoration.bold, Color.red);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String inp = input == null ? "" : String.join(" ", input).trim();
|
|
||||||
runFlowByNameFragment(name, inp, out, rpcOps, ansiProgressRenderer != null ? ansiProgressRenderer : new CRaSHANSIProgressRenderer(out), om);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command
|
|
||||||
@Usage("List flows that user can start.")
|
|
||||||
public void list(InvocationContext<String> context) throws Exception {
|
|
||||||
logger.info("Executing command \"flow list\".");
|
|
||||||
for (String name : ops().registeredFlows()) {
|
|
||||||
context.provide(name + System.lineSeparator());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command
|
|
||||||
@Usage("Kill a flow that is running on this node.")
|
|
||||||
public void kill(
|
|
||||||
@Usage("The UUID for the flow that we wish to kill") @Argument String id
|
|
||||||
) {
|
|
||||||
logger.info("Executing command \"flow kill {}\".", id);
|
|
||||||
killFlowById(id, out, ops(), objectMapper(null));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,75 +0,0 @@
|
|||||||
package net.corda.tools.shell;
|
|
||||||
|
|
||||||
import net.corda.core.crypto.SecureHash;
|
|
||||||
import net.corda.core.crypto.SecureHashKt;
|
|
||||||
import net.corda.core.internal.VisibleForTesting;
|
|
||||||
import net.corda.core.messaging.CordaRPCOps;
|
|
||||||
import net.corda.core.messaging.StateMachineTransactionMapping;
|
|
||||||
import org.crsh.cli.Argument;
|
|
||||||
import org.crsh.cli.Command;
|
|
||||||
import org.crsh.cli.Man;
|
|
||||||
import org.crsh.cli.Named;
|
|
||||||
import org.crsh.cli.Usage;
|
|
||||||
import org.crsh.text.Color;
|
|
||||||
import org.crsh.text.Decoration;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Named("hashLookup")
|
|
||||||
public class HashLookupShellCommand extends CordaRpcOpsShellCommand {
|
|
||||||
private static Logger logger = LoggerFactory.getLogger(HashLookupShellCommand.class);
|
|
||||||
private static final String manualText ="Checks if a transaction matching a specified Id hash value is recorded on this node.\n\n" +
|
|
||||||
"Both the transaction Id and the hashed value of a transaction Id (as returned by the Notary in case of a double-spend) is a valid input.\n" +
|
|
||||||
"This is mainly intended to be used for troubleshooting notarisation issues when a\n" +
|
|
||||||
"state is claimed to be already consumed by another transaction.\n\n" +
|
|
||||||
"Example usage: hashLookup E470FD8A6350A74217B0A99EA5FB71F091C84C64AD0DE0E72ECC10421D03AAC9";
|
|
||||||
|
|
||||||
@Command
|
|
||||||
@Man(manualText)
|
|
||||||
|
|
||||||
public void main(@Usage("A transaction Id or a hexadecimal SHA-256 hash value representing the hashed transaction Id") @Argument(unquote = false) String txIdHash) {
|
|
||||||
CordaRPCOps proxy = ops();
|
|
||||||
try {
|
|
||||||
hashLookup(out, proxy, txIdHash);
|
|
||||||
} catch (IllegalArgumentException ex) {
|
|
||||||
out.println(manualText);
|
|
||||||
out.println(ex.getMessage(), Decoration.bold, Color.red);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
protected static void hashLookup(PrintWriter out, CordaRPCOps proxy, String txIdHash) throws IllegalArgumentException {
|
|
||||||
logger.info("Executing command \"hashLookup\".");
|
|
||||||
|
|
||||||
if (txIdHash == null) {
|
|
||||||
out.println(manualText);
|
|
||||||
throw new IllegalArgumentException("Please provide a hexadecimal transaction Id hash value or a transaction Id");
|
|
||||||
}
|
|
||||||
|
|
||||||
SecureHash txIdHashParsed;
|
|
||||||
try {
|
|
||||||
txIdHashParsed = SecureHash.create(txIdHash);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
throw new IllegalArgumentException("The provided string is neither a valid SHA-256 hash value or a supported hash algorithm");
|
|
||||||
}
|
|
||||||
|
|
||||||
List<StateMachineTransactionMapping> mapping = proxy.stateMachineRecordedTransactionMappingSnapshot();
|
|
||||||
Optional<SecureHash> match = mapping.stream()
|
|
||||||
.map(StateMachineTransactionMapping::getTransactionId)
|
|
||||||
.filter(
|
|
||||||
txId -> txId.equals(txIdHashParsed) || SecureHash.hashAs(SecureHashKt.getAlgorithm(txIdHashParsed), txId.getBytes()).equals(txIdHashParsed)
|
|
||||||
)
|
|
||||||
.findFirst();
|
|
||||||
|
|
||||||
if (match.isPresent()) {
|
|
||||||
SecureHash found = match.get();
|
|
||||||
out.println("Found a matching transaction with Id: " + found.toString());
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("No matching transaction found");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,62 +0,0 @@
|
|||||||
package net.corda.tools.shell;
|
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
|
||||||
import com.google.common.collect.BiMap;
|
|
||||||
import com.google.common.collect.ImmutableBiMap;
|
|
||||||
import net.corda.tools.shell.InteractiveShell.OutputFormat;
|
|
||||||
import org.crsh.cli.Argument;
|
|
||||||
import org.crsh.cli.Command;
|
|
||||||
import org.crsh.cli.Man;
|
|
||||||
import org.crsh.cli.Named;
|
|
||||||
import org.crsh.cli.Usage;
|
|
||||||
import org.crsh.command.InvocationContext;
|
|
||||||
import org.crsh.command.ScriptException;
|
|
||||||
import org.crsh.text.RenderPrintWriter;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Man("Allows you to see and update the format that's currently used for the commands' output.")
|
|
||||||
@Usage("Allows you to see and update the format that's currently used for the commands' output.")
|
|
||||||
@Named("output-format")
|
|
||||||
public class OutputFormatCommand extends CordaRpcOpsShellCommand {
|
|
||||||
|
|
||||||
public OutputFormatCommand() {}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
OutputFormatCommand(final RenderPrintWriter printWriter) {
|
|
||||||
this.out = printWriter;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final BiMap<String, OutputFormat> OUTPUT_FORMAT_MAPPING = ImmutableBiMap.of(
|
|
||||||
"json", OutputFormat.JSON,
|
|
||||||
"yaml", OutputFormat.YAML
|
|
||||||
);
|
|
||||||
|
|
||||||
@Command
|
|
||||||
@Man("Sets the output format of the commands.")
|
|
||||||
@Usage("sets the output format of the commands.")
|
|
||||||
public void set(InvocationContext<Map> context,
|
|
||||||
@Usage("The format of the commands output. Supported values: json, yaml.") @Argument String format) {
|
|
||||||
OutputFormat outputFormat = parseFormat(format);
|
|
||||||
|
|
||||||
InteractiveShell.setOutputFormat(outputFormat);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command
|
|
||||||
@Man("Shows the output format of the commands.")
|
|
||||||
@Usage("shows the output format of the commands.")
|
|
||||||
public void get(InvocationContext<Map> context) {
|
|
||||||
OutputFormat outputFormat = InteractiveShell.getOutputFormat();
|
|
||||||
final String format = OUTPUT_FORMAT_MAPPING.inverse().get(outputFormat);
|
|
||||||
|
|
||||||
out.println(format);
|
|
||||||
}
|
|
||||||
|
|
||||||
private OutputFormat parseFormat(String format) {
|
|
||||||
if (!OUTPUT_FORMAT_MAPPING.containsKey(format)) {
|
|
||||||
throw new ScriptException("The provided format is not supported: " + format);
|
|
||||||
}
|
|
||||||
|
|
||||||
return OUTPUT_FORMAT_MAPPING.get(format);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,86 +0,0 @@
|
|||||||
package net.corda.tools.shell;
|
|
||||||
|
|
||||||
import com.google.common.collect.Maps;
|
|
||||||
import net.corda.client.jackson.StringToMethodCallParser;
|
|
||||||
import net.corda.core.messaging.CordaRPCOps;
|
|
||||||
import org.crsh.cli.Argument;
|
|
||||||
import org.crsh.cli.Command;
|
|
||||||
import org.crsh.cli.Man;
|
|
||||||
import org.crsh.cli.Named;
|
|
||||||
import org.crsh.cli.Usage;
|
|
||||||
import org.crsh.command.InvocationContext;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.util.AbstractMap;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import static java.util.Comparator.comparing;
|
|
||||||
|
|
||||||
// Note that this class cannot be converted to Kotlin because CRaSH does not understand InvocationContext<Map<?, ?>> which
|
|
||||||
// is the closest you can get in Kotlin to raw types.
|
|
||||||
|
|
||||||
@Named("run")
|
|
||||||
public class RunShellCommand extends CordaRpcOpsShellCommand {
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(RunShellCommand.class);
|
|
||||||
|
|
||||||
@Command
|
|
||||||
@Man(
|
|
||||||
"Runs a method from the CordaRPCOps interface, which is the same interface exposed to RPC clients.\n\n" +
|
|
||||||
|
|
||||||
"You can learn more about what commands are available by typing 'run' just by itself, or by\n" +
|
|
||||||
"consulting the developer guide at https://docs.corda.net/api/kotlin/corda/net.corda.core.messaging/-corda-r-p-c-ops/index.html"
|
|
||||||
)
|
|
||||||
@Usage("runs a method from the CordaRPCOps interface on the node.")
|
|
||||||
public Object main(InvocationContext<Map> context,
|
|
||||||
@Usage("The command to run") @Argument(unquote = false) List<String> command) {
|
|
||||||
logger.info("Executing command \"run {}\",", (command != null) ? String.join(" ", command) : "<no arguments>");
|
|
||||||
|
|
||||||
if (command == null) {
|
|
||||||
emitHelp(context);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return InteractiveShell.runRPCFromString(command, out, context, ops(), objectMapper(InteractiveShell.getCordappsClassloader()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void emitHelp(InvocationContext<Map> context) {
|
|
||||||
StringToMethodCallParser<CordaRPCOps> cordaRpcOpsParser =
|
|
||||||
new StringToMethodCallParser<>(
|
|
||||||
CordaRPCOps.class, objectMapper(InteractiveShell.getCordappsClassloader()));
|
|
||||||
|
|
||||||
// Sends data down the pipeline about what commands are available. CRaSH will render it nicely.
|
|
||||||
// Each element we emit is a map of column -> content.
|
|
||||||
Set<Map.Entry<String, String>> entries = cordaRpcOpsParser.getAvailableCommands().entrySet();
|
|
||||||
List<Map.Entry<String, String>> entryList = new ArrayList<>(entries);
|
|
||||||
|
|
||||||
entryList.add(new AbstractMap.SimpleEntry<>("gracefulShutdown", ""));//Shell only command
|
|
||||||
|
|
||||||
entryList.sort(comparing(Map.Entry::getKey));
|
|
||||||
for (Map.Entry<String, String> entry : entryList) {
|
|
||||||
// Skip these entries as they aren't really interesting for the user.
|
|
||||||
if (entry.getKey().equals("startFlowDynamic")) continue;
|
|
||||||
if (entry.getKey().equals("getProtocolVersion")) continue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
context.provide(commandAndDesc(entry.getKey(), entry.getValue()));
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private Map<String, String> commandAndDesc(String command, String description) {
|
|
||||||
// Use a LinkedHashMap to ensure that the Command column comes first.
|
|
||||||
Map<String, String> abruptShutdown = Maps.newLinkedHashMap();
|
|
||||||
abruptShutdown.put("Command", command);
|
|
||||||
abruptShutdown.put("Parameter types", description);
|
|
||||||
return abruptShutdown;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
package net.corda.tools.shell;
|
|
||||||
|
|
||||||
import net.corda.core.messaging.RPCOps;
|
|
||||||
import org.crsh.auth.AuthInfo;
|
|
||||||
|
|
||||||
public interface SshAuthInfo extends AuthInfo {
|
|
||||||
<T extends RPCOps> T getOrCreateRpcOps(Class<T> rpcOpsClass);
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
package net.corda.tools.shell;
|
|
||||||
|
|
||||||
// A simple forwarder to the "flow start" command, for easier typing.
|
|
||||||
|
|
||||||
import net.corda.tools.shell.utlities.ANSIProgressRenderer;
|
|
||||||
import net.corda.tools.shell.utlities.CRaSHANSIProgressRenderer;
|
|
||||||
import org.crsh.cli.*;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import static java.util.stream.Collectors.joining;
|
|
||||||
|
|
||||||
@Named("start")
|
|
||||||
public class StartShellCommand extends CordaRpcOpsShellCommand {
|
|
||||||
|
|
||||||
private static Logger logger = LoggerFactory.getLogger(StartShellCommand.class);
|
|
||||||
|
|
||||||
@Command
|
|
||||||
@Man("An alias for 'flow start'. Example: \"start Yo target: Some other company\"")
|
|
||||||
public void main(@Usage("The class name of the flow to run, or an unambiguous substring") @Argument String name,
|
|
||||||
@Usage("The data to pass as input") @Argument(unquote = false) List<String> input) {
|
|
||||||
|
|
||||||
logger.info("Executing command \"start {} {}\",", name, (input != null) ? input.stream().collect(joining(" ")) : "<no arguments>");
|
|
||||||
ANSIProgressRenderer ansiProgressRenderer = ansiProgressRenderer();
|
|
||||||
FlowShellCommand.startFlow(name, input, out, ops(), ansiProgressRenderer != null ? ansiProgressRenderer : new CRaSHANSIProgressRenderer(out), objectMapper(null));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
package net.corda.tools.shell
|
|
||||||
|
|
||||||
import net.corda.core.internal.AttachmentTrustInfo
|
|
||||||
import net.corda.core.internal.P2P_UPLOADER
|
|
||||||
import org.crsh.text.Color
|
|
||||||
import org.crsh.text.Decoration
|
|
||||||
import org.crsh.text.RenderPrintWriter
|
|
||||||
import org.crsh.text.ui.LabelElement
|
|
||||||
import org.crsh.text.ui.Overflow
|
|
||||||
import org.crsh.text.ui.RowElement
|
|
||||||
import org.crsh.text.ui.TableElement
|
|
||||||
|
|
||||||
class AttachmentTrustTable(
|
|
||||||
writer: RenderPrintWriter,
|
|
||||||
private val attachmentTrustInfos: List<AttachmentTrustInfo>
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val content: TableElement
|
|
||||||
|
|
||||||
init {
|
|
||||||
content = createTable()
|
|
||||||
createRows()
|
|
||||||
writer.print(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createTable(): TableElement {
|
|
||||||
val table = TableElement(2, 3, 1, 1, 3).overflow(Overflow.WRAP).rightCellPadding(3)
|
|
||||||
val header =
|
|
||||||
RowElement(true).add("Name", "Attachment ID", "Installed", "Trusted", "Trust Root").style(
|
|
||||||
Decoration.bold.fg(
|
|
||||||
Color.black
|
|
||||||
).bg(Color.white)
|
|
||||||
)
|
|
||||||
table.add(header)
|
|
||||||
return table
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createRows() {
|
|
||||||
for (info in attachmentTrustInfos) {
|
|
||||||
info.run {
|
|
||||||
val name = when {
|
|
||||||
fileName != null -> fileName!!
|
|
||||||
uploader?.startsWith(P2P_UPLOADER) ?: false -> {
|
|
||||||
"Received from: ${uploader!!.removePrefix("$P2P_UPLOADER:")}"
|
|
||||||
}
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
content.add(
|
|
||||||
RowElement().add(
|
|
||||||
LabelElement(name),
|
|
||||||
LabelElement(attachmentId),
|
|
||||||
LabelElement(isTrustRoot),
|
|
||||||
LabelElement(isTrusted),
|
|
||||||
LabelElement(trustRootFileName ?: trustRootId ?: "")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
package net.corda.tools.shell
|
|
||||||
|
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
|
||||||
import net.corda.core.utilities.loggerFor
|
|
||||||
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
|
|
||||||
import org.crsh.auth.AuthInfo
|
|
||||||
import org.crsh.auth.AuthenticationPlugin
|
|
||||||
import org.crsh.plugin.CRaSHPlugin
|
|
||||||
|
|
||||||
internal class CordaAuthenticationPlugin(private val rpcOpsProducer: RPCOpsProducer) : CRaSHPlugin<AuthenticationPlugin<String>>(), AuthenticationPlugin<String> {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val logger = loggerFor<CordaAuthenticationPlugin>()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getImplementation(): AuthenticationPlugin<String> = this
|
|
||||||
|
|
||||||
override fun getName(): String = "corda"
|
|
||||||
|
|
||||||
override fun authenticate(username: String?, credential: String?): AuthInfo {
|
|
||||||
|
|
||||||
if (username == null || credential == null) {
|
|
||||||
return AuthInfo.UNSUCCESSFUL
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
val cordaSSHAuthInfo = CordaSSHAuthInfo(rpcOpsProducer, username, credential, isSsh = true)
|
|
||||||
// We cannot guarantee authentication happened successfully till `RCPClient` session been established, hence doing a dummy call
|
|
||||||
cordaSSHAuthInfo.getOrCreateRpcOps(CordaRPCOps::class.java).protocolVersion
|
|
||||||
return cordaSSHAuthInfo
|
|
||||||
} catch (e: ActiveMQSecurityException) {
|
|
||||||
logger.warn(e.message)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logger.warn(e.message, e)
|
|
||||||
}
|
|
||||||
return AuthInfo.UNSUCCESSFUL
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCredentialType(): Class<String> = String::class.java
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
package net.corda.tools.shell
|
|
||||||
|
|
||||||
import org.crsh.auth.AuthInfo
|
|
||||||
import org.crsh.auth.DisconnectPlugin
|
|
||||||
import org.crsh.plugin.CRaSHPlugin
|
|
||||||
|
|
||||||
class CordaDisconnectPlugin : CRaSHPlugin<DisconnectPlugin>(), DisconnectPlugin {
|
|
||||||
override fun getImplementation() = this
|
|
||||||
|
|
||||||
override fun onDisconnect(userName: String?, authInfo: AuthInfo?) {
|
|
||||||
(authInfo as? CordaSSHAuthInfo)?.cleanUp()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
package net.corda.tools.shell
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import com.fasterxml.jackson.databind.type.TypeFactory
|
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
|
||||||
|
|
||||||
internal abstract class CordaRpcOpsShellCommand : InteractiveShellCommand<CordaRPCOps>() {
|
|
||||||
override val rpcOpsClass: Class<out CordaRPCOps> = CordaRPCOps::class.java
|
|
||||||
|
|
||||||
fun objectMapper(classLoader: ClassLoader?): ObjectMapper {
|
|
||||||
val om = createYamlInputMapper()
|
|
||||||
if (classLoader != null) {
|
|
||||||
om.typeFactory = TypeFactory.defaultInstance().withClassLoader(classLoader)
|
|
||||||
}
|
|
||||||
return om
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createYamlInputMapper(): ObjectMapper {
|
|
||||||
val rpcOps = ops()
|
|
||||||
return InteractiveShell.createYamlInputMapper(rpcOps)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
package net.corda.tools.shell
|
|
||||||
|
|
||||||
import com.github.benmanes.caffeine.cache.CacheLoader
|
|
||||||
import com.github.benmanes.caffeine.cache.Caffeine
|
|
||||||
import com.github.benmanes.caffeine.cache.RemovalListener
|
|
||||||
import com.google.common.util.concurrent.MoreExecutors
|
|
||||||
import net.corda.client.rpc.RPCConnection
|
|
||||||
import net.corda.core.internal.utilities.InvocationHandlerTemplate
|
|
||||||
import net.corda.core.messaging.RPCOps
|
|
||||||
import net.corda.tools.shell.utlities.ANSIProgressRenderer
|
|
||||||
import java.lang.reflect.Proxy
|
|
||||||
|
|
||||||
internal class CordaSSHAuthInfo(private val rpcOpsProducer: RPCOpsProducer,
|
|
||||||
private val username: String, private val credential: String, val ansiProgressRenderer: ANSIProgressRenderer? = null,
|
|
||||||
val isSsh: Boolean = false) : SshAuthInfo {
|
|
||||||
override fun isSuccessful(): Boolean = true
|
|
||||||
|
|
||||||
/**
|
|
||||||
* It is necessary to have a cache to prevent creation of too many proxies for the same class. Proxy ensures that RPC connections gracefully
|
|
||||||
* closed when cache entry is eliminated
|
|
||||||
*/
|
|
||||||
private val proxiesCache = Caffeine.newBuilder()
|
|
||||||
.maximumSize(10)
|
|
||||||
.removalListener(RemovalListener<Class<out RPCOps>, Pair<RPCOps, RPCConnection<RPCOps>>> { _, value, _ -> value?.second?.close() })
|
|
||||||
.executor(MoreExecutors.directExecutor())
|
|
||||||
.build(CacheLoader<Class<out RPCOps>, Pair<RPCOps, RPCConnection<RPCOps>>> { key -> createRpcOps(key) })
|
|
||||||
|
|
||||||
override fun <T : RPCOps> getOrCreateRpcOps(rpcOpsClass: Class<T>): T {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
return proxiesCache.get(rpcOpsClass)!!.first as T
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cleanUp() {
|
|
||||||
proxiesCache.asMap().forEach {
|
|
||||||
proxiesCache.invalidate(it.key)
|
|
||||||
it.value.second.forceClose()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <T : RPCOps> createRpcOps(rpcOpsClass: Class<out T>): Pair<T, RPCConnection<T>> {
|
|
||||||
val producerResult = rpcOpsProducer(username, credential, rpcOpsClass)
|
|
||||||
val anotherProxy = proxyRPCOps(producerResult.proxy, rpcOpsClass)
|
|
||||||
return anotherProxy to producerResult
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <T : RPCOps> proxyRPCOps(instance: T, rpcOpsClass: Class<out T>): T {
|
|
||||||
require(rpcOpsClass.isInterface) { "$rpcOpsClass must be an interface" }
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
return Proxy.newProxyInstance(rpcOpsClass.classLoader, arrayOf(rpcOpsClass), object : InvocationHandlerTemplate {
|
|
||||||
override val delegate = instance
|
|
||||||
}) as T
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,126 +0,0 @@
|
|||||||
package net.corda.tools.shell
|
|
||||||
|
|
||||||
import net.corda.core.flows.StateMachineRunId
|
|
||||||
import net.corda.core.internal.concurrent.openFuture
|
|
||||||
import net.corda.core.context.InvocationContext
|
|
||||||
import net.corda.core.messaging.StateMachineUpdate
|
|
||||||
import net.corda.core.messaging.StateMachineUpdate.Added
|
|
||||||
import net.corda.core.messaging.StateMachineUpdate.Removed
|
|
||||||
import net.corda.core.transactions.SignedTransaction
|
|
||||||
import net.corda.core.utilities.Try
|
|
||||||
import org.crsh.text.Color
|
|
||||||
import org.crsh.text.Decoration
|
|
||||||
import org.crsh.text.RenderPrintWriter
|
|
||||||
import org.crsh.text.ui.LabelElement
|
|
||||||
import org.crsh.text.ui.Overflow
|
|
||||||
import org.crsh.text.ui.RowElement
|
|
||||||
import org.crsh.text.ui.TableElement
|
|
||||||
import rx.Subscriber
|
|
||||||
|
|
||||||
class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Subscriber<Any>() {
|
|
||||||
private val indexMap = HashMap<StateMachineRunId, Int>()
|
|
||||||
private val table = createStateMachinesTable()
|
|
||||||
val future = openFuture<Unit>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
// The future is public and can be completed by something else to indicate we don't wish to follow
|
|
||||||
// anymore (e.g. the user pressing Ctrl-C).
|
|
||||||
future.then { unsubscribe() }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun onCompleted() {
|
|
||||||
// The observable of state machines will never complete.
|
|
||||||
future.set(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun onNext(t: Any?) {
|
|
||||||
if (t is StateMachineUpdate) {
|
|
||||||
toStream.cls()
|
|
||||||
createStateMachinesRow(t)
|
|
||||||
toStream.print(table)
|
|
||||||
toStream.println("Waiting for completion or Ctrl-C ... ")
|
|
||||||
toStream.flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun onError(e: Throwable) {
|
|
||||||
toStream.println("Observable completed with an error")
|
|
||||||
future.setException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stateColor(update: StateMachineUpdate): Color {
|
|
||||||
return when (update) {
|
|
||||||
is Added -> Color.blue
|
|
||||||
is Removed -> if (update.result.isSuccess) Color.green else Color.red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createStateMachinesTable(): TableElement {
|
|
||||||
val table = TableElement(1, 2, 1, 2).overflow(Overflow.HIDDEN).rightCellPadding(1)
|
|
||||||
val header = RowElement(true).add("Id", "Flow name", "Initiator", "Status").style(Decoration.bold.fg(Color.black).bg(Color.white))
|
|
||||||
table.add(header)
|
|
||||||
return table
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Add progress tracker?
|
|
||||||
private fun createStateMachinesRow(smmUpdate: StateMachineUpdate) {
|
|
||||||
when (smmUpdate) {
|
|
||||||
is Added -> {
|
|
||||||
table.add(RowElement().add(
|
|
||||||
LabelElement(formatFlowId(smmUpdate.id)),
|
|
||||||
LabelElement(formatFlowName(smmUpdate.stateMachineInfo.flowLogicClassName)),
|
|
||||||
LabelElement(formatInvocationContext(smmUpdate.stateMachineInfo.invocationContext)),
|
|
||||||
LabelElement("In progress")
|
|
||||||
).style(stateColor(smmUpdate).fg()))
|
|
||||||
indexMap[smmUpdate.id] = table.rows.size - 1
|
|
||||||
}
|
|
||||||
is Removed -> {
|
|
||||||
val idx = indexMap[smmUpdate.id]
|
|
||||||
if (idx != null) {
|
|
||||||
val oldRow = table.rows[idx]
|
|
||||||
val flowNameLabel = oldRow.getCol(1) as LabelElement
|
|
||||||
val flowInitiatorLabel = oldRow.getCol(2) as LabelElement
|
|
||||||
table.rows[idx] = RowElement().add(
|
|
||||||
LabelElement(formatFlowId(smmUpdate.id)),
|
|
||||||
LabelElement(flowNameLabel.value),
|
|
||||||
LabelElement(flowInitiatorLabel.value),
|
|
||||||
LabelElement(formatFlowResult(smmUpdate.result))
|
|
||||||
).style(stateColor(smmUpdate).fg())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun formatFlowName(flowName: String): String {
|
|
||||||
val camelCaseRegex = Regex("(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])")
|
|
||||||
val name = flowName.split('.', '$').last()
|
|
||||||
// Split CamelCase and get rid of "flow" at the end if present.
|
|
||||||
return camelCaseRegex.split(name).filter { it.compareTo("Flow", true) != 0 }.joinToString(" ")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun formatFlowId(flowId: StateMachineRunId): String {
|
|
||||||
return flowId.toString().removeSurrounding("[", "]")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun formatInvocationContext(context: InvocationContext): String {
|
|
||||||
return context.principal().name
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun formatFlowResult(flowResult: Try<*>): String {
|
|
||||||
fun successFormat(value: Any?): String {
|
|
||||||
return when (value) {
|
|
||||||
is SignedTransaction -> "Tx ID: " + value.id.toString()
|
|
||||||
is kotlin.Unit -> "No return value"
|
|
||||||
null -> "No return value"
|
|
||||||
else -> value.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return when (flowResult) {
|
|
||||||
is Try.Success -> successFormat(flowResult.value)
|
|
||||||
is Try.Failure -> flowResult.exception.message ?: flowResult.exception.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,783 +0,0 @@
|
|||||||
package net.corda.tools.shell
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonFactory
|
|
||||||
import com.fasterxml.jackson.databind.JsonMappingException
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature
|
|
||||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
|
||||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
|
|
||||||
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator
|
|
||||||
import net.corda.client.jackson.JacksonSupport
|
|
||||||
import net.corda.client.jackson.StringToMethodCallParser
|
|
||||||
import net.corda.client.rpc.PermissionException
|
|
||||||
import net.corda.client.rpc.RPCConnection
|
|
||||||
import net.corda.client.rpc.internal.RPCUtils.isShutdownMethodName
|
|
||||||
import net.corda.client.rpc.notUsed
|
|
||||||
import net.corda.core.CordaException
|
|
||||||
import net.corda.core.concurrent.CordaFuture
|
|
||||||
import net.corda.core.contracts.UniqueIdentifier
|
|
||||||
import net.corda.core.flows.FlowLogic
|
|
||||||
import net.corda.core.flows.StateMachineRunId
|
|
||||||
import net.corda.core.internal.Emoji
|
|
||||||
import net.corda.core.internal.VisibleForTesting
|
|
||||||
import net.corda.core.internal.concurrent.doneFuture
|
|
||||||
import net.corda.core.internal.concurrent.openFuture
|
|
||||||
import net.corda.core.internal.createDirectories
|
|
||||||
import net.corda.core.internal.div
|
|
||||||
import net.corda.core.internal.messaging.AttachmentTrustInfoRPCOps
|
|
||||||
import net.corda.core.internal.packageName_
|
|
||||||
import net.corda.core.internal.rootCause
|
|
||||||
import net.corda.core.internal.uncheckedCast
|
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
|
||||||
import net.corda.core.messaging.DataFeed
|
|
||||||
import net.corda.core.messaging.FlowProgressHandle
|
|
||||||
import net.corda.core.messaging.StateMachineUpdate
|
|
||||||
import net.corda.core.messaging.flows.FlowManagerRPCOps
|
|
||||||
import net.corda.core.messaging.pendingFlowsCount
|
|
||||||
import net.corda.tools.shell.utlities.ANSIProgressRenderer
|
|
||||||
import net.corda.tools.shell.utlities.StdoutANSIProgressRenderer
|
|
||||||
import org.crsh.command.InvocationContext
|
|
||||||
import org.crsh.command.ShellSafety
|
|
||||||
import org.crsh.console.jline.JLineProcessor
|
|
||||||
import org.crsh.console.jline.TerminalFactory
|
|
||||||
import org.crsh.console.jline.console.ConsoleReader
|
|
||||||
import org.crsh.lang.impl.java.JavaLanguage
|
|
||||||
import org.crsh.plugin.CRaSHPlugin
|
|
||||||
import org.crsh.plugin.PluginContext
|
|
||||||
import org.crsh.plugin.PluginLifeCycle
|
|
||||||
import org.crsh.plugin.ServiceLoaderDiscovery
|
|
||||||
import org.crsh.shell.Shell
|
|
||||||
import org.crsh.shell.ShellFactory
|
|
||||||
import org.crsh.shell.impl.command.ExternalResolver
|
|
||||||
import org.crsh.text.Color
|
|
||||||
import org.crsh.text.Decoration
|
|
||||||
import org.crsh.text.RenderPrintWriter
|
|
||||||
import org.crsh.util.InterruptHandler
|
|
||||||
import org.crsh.util.Utils
|
|
||||||
import org.crsh.vfs.FS
|
|
||||||
import org.crsh.vfs.spi.file.FileMountFactory
|
|
||||||
import org.crsh.vfs.spi.url.ClassPathMountFactory
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import rx.Observable
|
|
||||||
import rx.Subscriber
|
|
||||||
import java.io.FileDescriptor
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.PrintWriter
|
|
||||||
import java.lang.reflect.GenericArrayType
|
|
||||||
import java.lang.reflect.InvocationTargetException
|
|
||||||
import java.lang.reflect.ParameterizedType
|
|
||||||
import java.lang.reflect.Type
|
|
||||||
import java.lang.reflect.UndeclaredThrowableException
|
|
||||||
import java.nio.file.Path
|
|
||||||
import java.util.Properties
|
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
import java.util.concurrent.ExecutionException
|
|
||||||
import java.util.concurrent.Future
|
|
||||||
import kotlin.concurrent.thread
|
|
||||||
|
|
||||||
// TODO: Add command history.
|
|
||||||
// TODO: Command completion.
|
|
||||||
// TODO: Do something sensible with commands that return a future.
|
|
||||||
// TODO: Configure default renderers, send objects down the pipeline, add support for xml output format.
|
|
||||||
// TODO: Add a command to view last N lines/tail/control log4j2 loggers.
|
|
||||||
// TODO: Review or fix the JVM commands which have bitrotted and some are useless.
|
|
||||||
// TODO: Get rid of the 'java' command, it's kind of worthless.
|
|
||||||
// TODO: Fix up the 'dashboard' command which has some rendering issues.
|
|
||||||
// TODO: Resurrect or reimplement the mail plugin.
|
|
||||||
// TODO: Make it notice new shell commands added after the node started.
|
|
||||||
|
|
||||||
const val STANDALONE_SHELL_PERMISSION = "ALL"
|
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
|
||||||
object InteractiveShell {
|
|
||||||
private val log = LoggerFactory.getLogger(javaClass)
|
|
||||||
private lateinit var rpcOpsProducer: RPCOpsProducer
|
|
||||||
private lateinit var startupValidation: Lazy<CordaRPCOps>
|
|
||||||
private var rpcConn: RPCConnection<CordaRPCOps>? = null
|
|
||||||
private var shell: Shell? = null
|
|
||||||
private var classLoader: ClassLoader? = null
|
|
||||||
private lateinit var shellConfiguration: ShellConfiguration
|
|
||||||
private var onExit: () -> Unit = {}
|
|
||||||
private const val uuidStringSize = 36
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getCordappsClassloader() = classLoader
|
|
||||||
|
|
||||||
enum class OutputFormat {
|
|
||||||
JSON,
|
|
||||||
YAML
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startShell(configuration: ShellConfiguration, classLoader: ClassLoader? = null, standalone: Boolean = false) {
|
|
||||||
rpcOpsProducer = DefaultRPCOpsProducer(configuration, classLoader, standalone)
|
|
||||||
launchShell(configuration, standalone, classLoader)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchShell(configuration: ShellConfiguration, standalone: Boolean, classLoader: ClassLoader? = null) {
|
|
||||||
shellConfiguration = configuration
|
|
||||||
InteractiveShell.classLoader = classLoader
|
|
||||||
val runSshDaemon = configuration.sshdPort != null
|
|
||||||
|
|
||||||
var runShellInSafeMode = true
|
|
||||||
if (!standalone) {
|
|
||||||
log.info("launchShell: User=${configuration.user} perm=${configuration.permissions}")
|
|
||||||
log.info("Shell: PermitExit= ${configuration.localShellAllowExitInSafeMode}, UNSAFELOCAL=${configuration.localShellUnsafe}")
|
|
||||||
runShellInSafeMode = configuration.permissions?.filter { it.contains(STANDALONE_SHELL_PERMISSION); }?.isEmpty() != false
|
|
||||||
}
|
|
||||||
|
|
||||||
val config = Properties()
|
|
||||||
if (runSshDaemon) {
|
|
||||||
// Enable SSH access. Note: these have to be strings, even though raw object assignments also work.
|
|
||||||
config["crash.ssh.port"] = configuration.sshdPort?.toString()
|
|
||||||
config["crash.auth"] = "corda"
|
|
||||||
configuration.sshHostKeyDirectory?.apply {
|
|
||||||
val sshKeysDir = configuration.sshHostKeyDirectory.createDirectories()
|
|
||||||
config["crash.ssh.keypath"] = (sshKeysDir / "hostkey.pem").toString()
|
|
||||||
config["crash.ssh.keygen"] = "true"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ExternalResolver.INSTANCE.addCommand(
|
|
||||||
"output-format",
|
|
||||||
"Commands to inspect and update the output format.",
|
|
||||||
OutputFormatCommand::class.java
|
|
||||||
)
|
|
||||||
ExternalResolver.INSTANCE.addCommand(
|
|
||||||
"run",
|
|
||||||
"Runs a method from the CordaRPCOps interface on the node.",
|
|
||||||
RunShellCommand::class.java
|
|
||||||
)
|
|
||||||
ExternalResolver.INSTANCE.addCommand(
|
|
||||||
"flow",
|
|
||||||
"Commands to work with flows. Flows are how you can change the ledger.",
|
|
||||||
FlowShellCommand::class.java
|
|
||||||
)
|
|
||||||
ExternalResolver.INSTANCE.addCommand(
|
|
||||||
"start",
|
|
||||||
"An alias for 'flow start'",
|
|
||||||
StartShellCommand::class.java
|
|
||||||
)
|
|
||||||
ExternalResolver.INSTANCE.addCommand(
|
|
||||||
"hashLookup",
|
|
||||||
"Checks if a transaction with matching Id hash exists.",
|
|
||||||
HashLookupShellCommand::class.java
|
|
||||||
)
|
|
||||||
ExternalResolver.INSTANCE.addCommand(
|
|
||||||
"attachments",
|
|
||||||
"Commands to extract information about attachments stored within the node",
|
|
||||||
AttachmentShellCommand::class.java
|
|
||||||
)
|
|
||||||
ExternalResolver.INSTANCE.addCommand(
|
|
||||||
"checkpoints",
|
|
||||||
"Commands to extract information about checkpoints stored within the node",
|
|
||||||
CheckpointShellCommand::class.java
|
|
||||||
)
|
|
||||||
|
|
||||||
val shellSafety = ShellSafety().apply {
|
|
||||||
setSafeShell(runShellInSafeMode)
|
|
||||||
setInternal(!standalone)
|
|
||||||
setStandAlone(standalone)
|
|
||||||
setAllowExitInSafeMode(configuration.localShellAllowExitInSafeMode || standalone)
|
|
||||||
}
|
|
||||||
shell = ShellLifecycle(configuration.commandsDirectory, shellSafety).start(config, configuration.user, configuration.password)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun runLocalShell(onExit: () -> Unit = {}) {
|
|
||||||
this.onExit = onExit
|
|
||||||
val terminal = TerminalFactory.create()
|
|
||||||
val consoleReader = ConsoleReader("Corda", FileInputStream(FileDescriptor.`in`), System.out, terminal)
|
|
||||||
val jlineProcessor = JLineProcessor(terminal.isAnsiSupported, shell, consoleReader, System.out)
|
|
||||||
InterruptHandler { jlineProcessor.interrupt() }.install()
|
|
||||||
thread(name = "Command line shell processor", isDaemon = true) {
|
|
||||||
Emoji.renderIfSupported {
|
|
||||||
try {
|
|
||||||
jlineProcessor.run()
|
|
||||||
} catch (e: IndexOutOfBoundsException) {
|
|
||||||
log.warn("Cannot parse malformed command.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
thread(name = "Command line shell terminator", isDaemon = true) {
|
|
||||||
// Wait for the shell to finish.
|
|
||||||
jlineProcessor.closed()
|
|
||||||
log.info("Command shell has exited")
|
|
||||||
terminal.restore()
|
|
||||||
onExit.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ShellLifecycle(private val shellCommands: Path, private val shellSafety: ShellSafety) : PluginLifeCycle() {
|
|
||||||
fun start(config: Properties, localUserName: String = "", localUserPassword: String = ""): Shell {
|
|
||||||
val classLoader = this.javaClass.classLoader
|
|
||||||
val classpathDriver = ClassPathMountFactory(classLoader)
|
|
||||||
val fileDriver = FileMountFactory(Utils.getCurrentDirectory())
|
|
||||||
|
|
||||||
val extraCommandsPath = shellCommands.toAbsolutePath().createDirectories()
|
|
||||||
val commandsFS = FS.Builder()
|
|
||||||
.register("file", fileDriver)
|
|
||||||
.mount("file:$extraCommandsPath")
|
|
||||||
.register("classpath", classpathDriver)
|
|
||||||
.mount("classpath:/net/corda/tools/shell/")
|
|
||||||
.mount("classpath:/crash/commands/")
|
|
||||||
.build()
|
|
||||||
val confFS = FS.Builder()
|
|
||||||
.register("classpath", classpathDriver)
|
|
||||||
.mount("classpath:/crash")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val discovery = object : ServiceLoaderDiscovery(classLoader) {
|
|
||||||
override fun getPlugins(): Iterable<CRaSHPlugin<*>> {
|
|
||||||
// Don't use the Java language plugin (we may not have tools.jar available at runtime), this
|
|
||||||
// will cause any commands using JIT Java compilation to be suppressed. In CRaSH upstream that
|
|
||||||
// is only the 'jmx' command.
|
|
||||||
return super.getPlugins().filterNot { it is JavaLanguage } + CordaAuthenticationPlugin(rpcOpsProducer) +
|
|
||||||
CordaDisconnectPlugin()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val attributes = emptyMap<String, Any>()
|
|
||||||
val context = PluginContext(discovery, attributes, commandsFS, confFS, classLoader)
|
|
||||||
context.refresh()
|
|
||||||
this.config = config
|
|
||||||
start(context)
|
|
||||||
startupValidation = lazy {
|
|
||||||
rpcOpsProducer(localUserName, localUserPassword, CordaRPCOps::class.java).let {
|
|
||||||
rpcConn = it
|
|
||||||
it.proxy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// For local shell create an artificial authInfo with super user permissions
|
|
||||||
val authInfo = CordaSSHAuthInfo(rpcOpsProducer, localUserName, localUserPassword, StdoutANSIProgressRenderer)
|
|
||||||
return context.getPlugin(ShellFactory::class.java).create(null, authInfo, shellSafety)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun nodeInfo() = try {
|
|
||||||
startupValidation.value.nodeInfo()
|
|
||||||
} catch (e: UndeclaredThrowableException) {
|
|
||||||
throw e.cause ?: e
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun setOutputFormat(outputFormat: OutputFormat) {
|
|
||||||
this.outputFormat = outputFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getOutputFormat(): OutputFormat {
|
|
||||||
return outputFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createYamlInputMapper(rpcOps: CordaRPCOps): ObjectMapper {
|
|
||||||
// Return a standard Corda Jackson object mapper, configured to use YAML by default and with extra
|
|
||||||
// serializers.
|
|
||||||
return JacksonSupport.createDefaultMapper(rpcOps, YAMLFactory(), true).apply {
|
|
||||||
val rpcModule = SimpleModule().apply {
|
|
||||||
addDeserializer(InputStream::class.java, InputStreamDeserializer)
|
|
||||||
addDeserializer(UniqueIdentifier::class.java, UniqueIdentifierDeserializer)
|
|
||||||
}
|
|
||||||
registerModule(rpcModule)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createOutputMapper(outputFormat: OutputFormat): ObjectMapper {
|
|
||||||
val factory = when(outputFormat) {
|
|
||||||
OutputFormat.JSON -> JsonFactory()
|
|
||||||
OutputFormat.YAML -> YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)
|
|
||||||
}
|
|
||||||
|
|
||||||
return JacksonSupport.createNonRpcMapper(factory).apply {
|
|
||||||
// Register serializers for stateful objects from libraries that are special to the RPC system and don't
|
|
||||||
// make sense to print out to the screen. For classes we own, annotations can be used instead.
|
|
||||||
val rpcModule = SimpleModule().apply {
|
|
||||||
addSerializer(Observable::class.java, ObservableSerializer)
|
|
||||||
addSerializer(InputStream::class.java, InputStreamSerializer)
|
|
||||||
}
|
|
||||||
registerModule(rpcModule)
|
|
||||||
|
|
||||||
disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
|
|
||||||
enable(SerializationFeature.INDENT_OUTPUT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: A default renderer could be used, instead of an object mapper. See: http://www.crashub.org/1.3/reference.html#_renderers
|
|
||||||
private var outputFormat = OutputFormat.YAML
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
lateinit var latch: CountDownLatch
|
|
||||||
private set
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called from the 'flow' shell command. Takes a name fragment and finds a matching flow, or prints out
|
|
||||||
* the list of options if the request is ambiguous. Then parses [inputData] as constructor arguments using
|
|
||||||
* the [runFlowFromString] method and starts the requested flow. Ctrl-C can be used to cancel.
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun runFlowByNameFragment(nameFragment: String,
|
|
||||||
inputData: String,
|
|
||||||
output: RenderPrintWriter,
|
|
||||||
rpcOps: CordaRPCOps,
|
|
||||||
ansiProgressRenderer: ANSIProgressRenderer,
|
|
||||||
inputObjectMapper: ObjectMapper = createYamlInputMapper(rpcOps)) {
|
|
||||||
val matches = try {
|
|
||||||
rpcOps.registeredFlows().filter { nameFragment in it }.sortedBy { it.length }
|
|
||||||
} catch (e: PermissionException) {
|
|
||||||
output.println(e.message ?: "Access denied", Decoration.bold, Color.red)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (matches.isEmpty()) {
|
|
||||||
output.println("No matching flow found, run 'flow list' to see your options.", Decoration.bold, Color.red)
|
|
||||||
return
|
|
||||||
} else if (matches.size > 1 && matches.find { it.endsWith(nameFragment)} == null) {
|
|
||||||
output.println("Ambiguous name provided, please be more specific. Your options are:")
|
|
||||||
matches.forEachIndexed { i, s -> output.println("${i + 1}. $s", Decoration.bold, Color.yellow) }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val flowName = matches.find { it.endsWith(nameFragment)} ?: matches.single()
|
|
||||||
val flowClazz: Class<FlowLogic<*>> = if (classLoader != null) {
|
|
||||||
uncheckedCast(Class.forName(flowName, true, classLoader))
|
|
||||||
} else {
|
|
||||||
uncheckedCast(Class.forName(flowName))
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// Show the progress tracker on the console until the flow completes or is interrupted with a
|
|
||||||
// Ctrl-C keypress.
|
|
||||||
val stateObservable = runFlowFromString(
|
|
||||||
{ clazz, args -> rpcOps.startTrackedFlowDynamic(clazz, *args) },
|
|
||||||
inputData,
|
|
||||||
flowClazz,
|
|
||||||
inputObjectMapper
|
|
||||||
)
|
|
||||||
|
|
||||||
latch = CountDownLatch(1)
|
|
||||||
ansiProgressRenderer.render(stateObservable, latch::countDown)
|
|
||||||
// Wait for the flow to end and the progress tracker to notice. By the time the latch is released
|
|
||||||
// the tracker is done with the screen.
|
|
||||||
while (!Thread.currentThread().isInterrupted) {
|
|
||||||
try {
|
|
||||||
latch.await()
|
|
||||||
break
|
|
||||||
} catch (e: InterruptedException) {
|
|
||||||
try {
|
|
||||||
rpcOps.killFlow(stateObservable.id)
|
|
||||||
} finally {
|
|
||||||
Thread.currentThread().interrupt()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output.println("Flow completed with result: ${stateObservable.returnValue.get()}")
|
|
||||||
} catch (e: NoApplicableConstructor) {
|
|
||||||
output.println("No matching constructor found:", Decoration.bold, Color.red)
|
|
||||||
e.errors.forEach { output.println("- $it", Decoration.bold, Color.red) }
|
|
||||||
} catch (e: PermissionException) {
|
|
||||||
output.println(e.message ?: "Access denied", Decoration.bold, Color.red)
|
|
||||||
} catch (e: ExecutionException) {
|
|
||||||
// ignoring it as already logged by the progress handler subscriber
|
|
||||||
} finally {
|
|
||||||
InputStreamDeserializer.closeAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NoApplicableConstructor(val errors: List<String>) : CordaException(this.toString()) {
|
|
||||||
override fun toString() =
|
|
||||||
(listOf("No applicable constructor for flow. Problems were:") + errors).joinToString(System.lineSeparator())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tidies up a possibly generic type name by chopping off the package names of classes in a hard-coded set of
|
|
||||||
* hierarchies that are known to be widely used and recognised, and also not have (m)any ambiguous names in them.
|
|
||||||
*
|
|
||||||
* This is used for printing error messages when something doesn't match.
|
|
||||||
*/
|
|
||||||
private fun maybeAbbreviateGenericType(type: Type, extraRecognisedPackage: String): String {
|
|
||||||
val packagesToAbbreviate = listOf("java.", "net.corda.core.", "kotlin.", extraRecognisedPackage)
|
|
||||||
|
|
||||||
fun shouldAbbreviate(typeName: String) = packagesToAbbreviate.any { typeName.startsWith(it) }
|
|
||||||
fun abbreviated(typeName: String) = if (shouldAbbreviate(typeName)) typeName.split('.').last() else typeName
|
|
||||||
|
|
||||||
fun innerLoop(type: Type): String = when (type) {
|
|
||||||
is ParameterizedType -> {
|
|
||||||
val args: List<String> = type.actualTypeArguments.map(::innerLoop)
|
|
||||||
abbreviated(type.rawType.typeName) + '<' + args.joinToString(", ") + '>'
|
|
||||||
}
|
|
||||||
is GenericArrayType -> {
|
|
||||||
innerLoop(type.genericComponentType) + "[]"
|
|
||||||
}
|
|
||||||
is Class<*> -> {
|
|
||||||
if (type.isArray)
|
|
||||||
abbreviated(type.simpleName)
|
|
||||||
else
|
|
||||||
abbreviated(type.name).replace('$', '.')
|
|
||||||
}
|
|
||||||
else -> type.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
return innerLoop(type)
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun killFlowById(id: String,
|
|
||||||
output: RenderPrintWriter,
|
|
||||||
rpcOps: CordaRPCOps,
|
|
||||||
inputObjectMapper: ObjectMapper = createYamlInputMapper(rpcOps)) {
|
|
||||||
try {
|
|
||||||
val runId = try {
|
|
||||||
inputObjectMapper.readValue(id, StateMachineRunId::class.java)
|
|
||||||
} catch (e: JsonMappingException) {
|
|
||||||
output.println("Cannot parse flow ID of '$id' - expecting a UUID.", Decoration.bold, Color.red)
|
|
||||||
log.error("Failed to parse flow ID", e)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
//auxiliary validation - workaround for JDK8 bug https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8159339
|
|
||||||
if (id.length < uuidStringSize) {
|
|
||||||
val msg = "Can not kill the flow. Flow ID of '$id' seems to be malformed - a UUID should have $uuidStringSize characters. " +
|
|
||||||
"Expand the terminal window to see the full UUID value."
|
|
||||||
output.println(msg, Decoration.bold, Color.red)
|
|
||||||
log.warn(msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (rpcOps.killFlow(runId)) {
|
|
||||||
output.println("Killed flow $runId", Decoration.bold, Color.yellow)
|
|
||||||
} else {
|
|
||||||
output.println("Failed to kill flow $runId", Decoration.bold, Color.red)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
output.flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a [FlowLogic] class and a string in one-line Yaml form, finds an applicable constructor and starts
|
|
||||||
* the flow, returning the created flow logic. Useful for lightweight invocation where text is preferable
|
|
||||||
* to statically typed, compiled code.
|
|
||||||
*
|
|
||||||
* See the [StringToMethodCallParser] class to learn more about limitations and acceptable syntax.
|
|
||||||
*
|
|
||||||
* @throws NoApplicableConstructor if no constructor could be found for the given set of types.
|
|
||||||
*/
|
|
||||||
@Throws(NoApplicableConstructor::class)
|
|
||||||
fun <T> runFlowFromString(invoke: (Class<out FlowLogic<T>>, Array<out Any?>) -> FlowProgressHandle<T>,
|
|
||||||
inputData: String,
|
|
||||||
clazz: Class<out FlowLogic<T>>,
|
|
||||||
om: ObjectMapper): FlowProgressHandle<T> {
|
|
||||||
|
|
||||||
val errors = ArrayList<String>()
|
|
||||||
val parser = StringToMethodCallParser(clazz, om)
|
|
||||||
val nameTypeList = getMatchingConstructorParamsAndTypes(parser, inputData, clazz)
|
|
||||||
|
|
||||||
try {
|
|
||||||
val args = parser.parseArguments(clazz.name, nameTypeList, inputData)
|
|
||||||
return invoke(clazz, args)
|
|
||||||
} catch (e: StringToMethodCallParser.UnparseableCallException.ReflectionDataMissing) {
|
|
||||||
val argTypes = nameTypeList.map { (_, type) -> type }
|
|
||||||
errors.add("$argTypes: <constructor missing parameter reflection data>")
|
|
||||||
} catch (e: StringToMethodCallParser.UnparseableCallException) {
|
|
||||||
val argTypes = nameTypeList.map { (_, type) -> type }
|
|
||||||
errors.add("$argTypes: ${e.message}")
|
|
||||||
}
|
|
||||||
throw NoApplicableConstructor(errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <T> getMatchingConstructorParamsAndTypes(parser: StringToMethodCallParser<FlowLogic<T>>,
|
|
||||||
inputData: String,
|
|
||||||
clazz: Class<out FlowLogic<T>>) : List<Pair<String, Type>> {
|
|
||||||
val errors = ArrayList<String>()
|
|
||||||
val classPackage = clazz.packageName_
|
|
||||||
lateinit var paramNamesFromConstructor: List<String>
|
|
||||||
|
|
||||||
for (ctor in clazz.constructors) { // Attempt construction with the given arguments.
|
|
||||||
|
|
||||||
fun getPrototype(): List<String> {
|
|
||||||
val argTypes = ctor.genericParameterTypes.map {
|
|
||||||
// If the type name is in the net.corda.core or java namespaces, chop off the package name
|
|
||||||
// because these hierarchies don't have (m)any ambiguous names and the extra detail is just noise.
|
|
||||||
maybeAbbreviateGenericType(it, classPackage)
|
|
||||||
}
|
|
||||||
return paramNamesFromConstructor.zip(argTypes).map { (name, type) -> "$name: $type" }
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
paramNamesFromConstructor = parser.paramNamesFromConstructor(ctor)
|
|
||||||
val nameTypeList = paramNamesFromConstructor.zip(ctor.genericParameterTypes)
|
|
||||||
parser.validateIsMatchingCtor(clazz.name, nameTypeList, inputData)
|
|
||||||
return nameTypeList
|
|
||||||
|
|
||||||
}
|
|
||||||
catch (e: StringToMethodCallParser.UnparseableCallException.MissingParameter) {
|
|
||||||
errors.add("${getPrototype()}: missing parameter ${e.paramName}")
|
|
||||||
}
|
|
||||||
catch (e: StringToMethodCallParser.UnparseableCallException.TooManyParameters) {
|
|
||||||
errors.add("${getPrototype()}: too many parameters")
|
|
||||||
}
|
|
||||||
catch (e: StringToMethodCallParser.UnparseableCallException.ReflectionDataMissing) {
|
|
||||||
val argTypes = ctor.genericParameterTypes.map { it.typeName }
|
|
||||||
errors.add("$argTypes: <constructor missing parameter reflection data>")
|
|
||||||
}
|
|
||||||
catch (e: StringToMethodCallParser.UnparseableCallException) {
|
|
||||||
val argTypes = ctor.genericParameterTypes.map { it.typeName }
|
|
||||||
errors.add("$argTypes: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw NoApplicableConstructor(errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Filtering on error/success when we will have some sort of flow auditing, for now it doesn't make much sense.
|
|
||||||
@JvmStatic
|
|
||||||
fun runStateMachinesView(out: RenderPrintWriter, rpcOps: CordaRPCOps): Any? {
|
|
||||||
val proxy = rpcOps
|
|
||||||
val (stateMachines, stateMachineUpdates) = proxy.stateMachinesFeed()
|
|
||||||
val currentStateMachines = stateMachines.map { StateMachineUpdate.Added(it) }
|
|
||||||
val subscriber = FlowWatchPrintingSubscriber(out)
|
|
||||||
stateMachineUpdates.startWith(currentStateMachines).subscribe(subscriber)
|
|
||||||
var result: Any? = subscriber.future
|
|
||||||
if (result is Future<*>) {
|
|
||||||
if (!result.isDone) {
|
|
||||||
out.cls()
|
|
||||||
out.println("Waiting for completion or Ctrl-C ... ")
|
|
||||||
out.flush()
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
result = result.get()
|
|
||||||
} catch (e: InterruptedException) {
|
|
||||||
subscriber.unsubscribe()
|
|
||||||
Thread.currentThread().interrupt()
|
|
||||||
} catch (e: ExecutionException) {
|
|
||||||
throw e.rootCause
|
|
||||||
} catch (e: InvocationTargetException) {
|
|
||||||
throw e.rootCause
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun runAttachmentTrustInfoView(
|
|
||||||
out: RenderPrintWriter,
|
|
||||||
rpcOps: AttachmentTrustInfoRPCOps
|
|
||||||
): Any {
|
|
||||||
return AttachmentTrustTable(out, rpcOps.attachmentTrustInfos)
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun runDumpCheckpoints(rpcOps: FlowManagerRPCOps) {
|
|
||||||
rpcOps.dumpCheckpoints()
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun runDebugCheckpoints(rpcOps: FlowManagerRPCOps) {
|
|
||||||
rpcOps.debugCheckpoints()
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun runRPCFromString(input: List<String>, out: RenderPrintWriter, context: InvocationContext<out Any>, cordaRPCOps: CordaRPCOps,
|
|
||||||
inputObjectMapper: ObjectMapper): Any? {
|
|
||||||
val cmd = input.joinToString(" ").trim { it <= ' ' }
|
|
||||||
if (cmd.startsWith("startflow", ignoreCase = true)) {
|
|
||||||
// The flow command provides better support and startFlow requires special handling anyway due to
|
|
||||||
// the generic startFlow RPC interface which offers no type information with which to parse the
|
|
||||||
// string form of the command.
|
|
||||||
out.println("Please use the 'flow' command to interact with flows rather than the 'run' command.", Decoration.bold, Color.yellow)
|
|
||||||
return null
|
|
||||||
} else if (cmd.substringAfter(" ").trim().equals("gracefulShutdown", ignoreCase = true)) {
|
|
||||||
return gracefulShutdown(out, cordaRPCOps)
|
|
||||||
}
|
|
||||||
|
|
||||||
var result: Any? = null
|
|
||||||
try {
|
|
||||||
InputStreamSerializer.invokeContext = context
|
|
||||||
val parser = StringToMethodCallParser(CordaRPCOps::class.java, inputObjectMapper)
|
|
||||||
val call = parser.parse(cordaRPCOps, cmd)
|
|
||||||
result = call.call()
|
|
||||||
var subscription : Subscriber<*>? = null
|
|
||||||
if (result != null && result !== Unit && result !is Void) {
|
|
||||||
val (subs, future) = printAndFollowRPCResponse(result, out, outputFormat)
|
|
||||||
subscription = subs
|
|
||||||
result = future
|
|
||||||
}
|
|
||||||
if (result is Future<*>) {
|
|
||||||
if (!result.isDone) {
|
|
||||||
out.println("Waiting for completion or Ctrl-C ... ")
|
|
||||||
out.flush()
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
result = result.get()
|
|
||||||
} catch (e: InterruptedException) {
|
|
||||||
subscription?.unsubscribe()
|
|
||||||
Thread.currentThread().interrupt()
|
|
||||||
} catch (e: ExecutionException) {
|
|
||||||
throw e.rootCause
|
|
||||||
} catch (e: InvocationTargetException) {
|
|
||||||
throw e.rootCause
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isShutdownMethodName(cmd)) {
|
|
||||||
out.println("Called 'shutdown' on the node.\nQuitting the shell now.").also { out.flush() }
|
|
||||||
onExit.invoke()
|
|
||||||
}
|
|
||||||
} catch (e: StringToMethodCallParser.UnparseableCallException) {
|
|
||||||
out.println(e.message, Decoration.bold, Color.red)
|
|
||||||
if (e !is StringToMethodCallParser.UnparseableCallException.NoSuchFile) {
|
|
||||||
out.println("Please try 'run -h' to learn what syntax is acceptable")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
out.println("RPC failed: ${e.rootCause}", Decoration.bold, Color.red)
|
|
||||||
} finally {
|
|
||||||
InputStreamSerializer.invokeContext = null
|
|
||||||
InputStreamDeserializer.closeAll()
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun gracefulShutdown(userSessionOut: RenderPrintWriter, cordaRPCOps: CordaRPCOps): Int {
|
|
||||||
|
|
||||||
var result = 0 // assume it all went well
|
|
||||||
|
|
||||||
fun display(statements: RenderPrintWriter.() -> Unit) {
|
|
||||||
statements.invoke(userSessionOut)
|
|
||||||
userSessionOut.flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
display {
|
|
||||||
println("Orchestrating a clean shutdown, press CTRL+C to cancel...")
|
|
||||||
println("...enabling draining mode")
|
|
||||||
println("...waiting for in-flight flows to be completed")
|
|
||||||
}
|
|
||||||
|
|
||||||
val latch = CountDownLatch(1)
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val subscription = cordaRPCOps.pendingFlowsCount().updates
|
|
||||||
.doAfterTerminate(latch::countDown)
|
|
||||||
.subscribe(
|
|
||||||
// For each update.
|
|
||||||
{ (completed, total) -> display { println("...remaining: $completed / $total") } },
|
|
||||||
// On error.
|
|
||||||
{
|
|
||||||
log.error(it.message)
|
|
||||||
throw it
|
|
||||||
},
|
|
||||||
// When completed.
|
|
||||||
{
|
|
||||||
// This will only show up in the standalone Shell, because the embedded one
|
|
||||||
// is killed as part of a node's shutdown.
|
|
||||||
display { println("...done, quitting the shell now.") }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
cordaRPCOps.terminate(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
latch.await()
|
|
||||||
// Unsubscribe or we hold up the shutdown
|
|
||||||
subscription.unsubscribe()
|
|
||||||
rpcConn?.forceClose()
|
|
||||||
onExit.invoke()
|
|
||||||
} catch (e: InterruptedException) {
|
|
||||||
// Cancelled whilst draining flows. So let's carry on from here
|
|
||||||
cordaRPCOps.setFlowsDrainingModeEnabled(false)
|
|
||||||
display { println("...cancelled clean shutdown.") }
|
|
||||||
result = 1
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
display { println("RPC failed: ${e.rootCause}", Decoration.bold, Color.red) }
|
|
||||||
result = 1
|
|
||||||
} finally {
|
|
||||||
InputStreamSerializer.invokeContext = null
|
|
||||||
InputStreamDeserializer.closeAll()
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun printAndFollowRPCResponse(
|
|
||||||
response: Any?,
|
|
||||||
out: PrintWriter,
|
|
||||||
outputFormat: OutputFormat
|
|
||||||
): Pair<PrintingSubscriber?, CordaFuture<Unit>> {
|
|
||||||
val outputMapper = createOutputMapper(outputFormat)
|
|
||||||
|
|
||||||
val mapElement: (Any?) -> String = { element -> outputMapper.writerWithDefaultPrettyPrinter().writeValueAsString(element) }
|
|
||||||
return maybeFollow(response, mapElement, out)
|
|
||||||
}
|
|
||||||
|
|
||||||
private class PrintingSubscriber(private val printerFun: (Any?) -> String, private val toStream: PrintWriter) : Subscriber<Any>() {
|
|
||||||
private var count = 0
|
|
||||||
val future = openFuture<Unit>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
// The future is public and can be completed by something else to indicate we don't wish to follow
|
|
||||||
// anymore (e.g. the user pressing Ctrl-C).
|
|
||||||
future.then { unsubscribe() }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun onCompleted() {
|
|
||||||
toStream.println("Observable has completed")
|
|
||||||
future.set(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun onNext(t: Any?) {
|
|
||||||
count++
|
|
||||||
toStream.println("Observation $count: " + printerFun(t))
|
|
||||||
toStream.flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
override fun onError(e: Throwable) {
|
|
||||||
toStream.println("Observable completed with an error")
|
|
||||||
e.printStackTrace(toStream)
|
|
||||||
future.setException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun maybeFollow(
|
|
||||||
response: Any?,
|
|
||||||
printerFun: (Any?) -> String,
|
|
||||||
out: PrintWriter
|
|
||||||
): Pair<PrintingSubscriber?, CordaFuture<Unit>> {
|
|
||||||
// Match on a couple of common patterns for "important" observables. It's tough to do this in a generic
|
|
||||||
// way because observables can be embedded anywhere in the object graph, and can emit other arbitrary
|
|
||||||
// object graphs that contain yet more observables. So we just look for top level responses that follow
|
|
||||||
// the standard "track" pattern, and print them until the user presses Ctrl-C
|
|
||||||
var result = Pair<PrintingSubscriber?, CordaFuture<Unit>>(null, doneFuture(Unit))
|
|
||||||
|
|
||||||
|
|
||||||
when {
|
|
||||||
response is DataFeed<*, *> -> {
|
|
||||||
out.println("Snapshot:")
|
|
||||||
out.println(printerFun(response.snapshot))
|
|
||||||
out.flush()
|
|
||||||
out.println("Updates:")
|
|
||||||
|
|
||||||
val unsubscribeAndPrint: (Any?) -> String = { resp ->
|
|
||||||
if (resp is StateMachineUpdate.Added) {
|
|
||||||
resp.stateMachineInfo.progressTrackerStepAndUpdates?.updates?.notUsed()
|
|
||||||
}
|
|
||||||
printerFun(resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
result = printNextElements(response.updates, unsubscribeAndPrint, out)
|
|
||||||
}
|
|
||||||
response is Observable<*> -> {
|
|
||||||
result = printNextElements(response, printerFun, out)
|
|
||||||
}
|
|
||||||
response != null -> {
|
|
||||||
out.println(printerFun(response))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun printNextElements(
|
|
||||||
elements: Observable<*>,
|
|
||||||
printerFun: (Any?) -> String,
|
|
||||||
out: PrintWriter
|
|
||||||
): Pair<PrintingSubscriber?, CordaFuture<Unit>> {
|
|
||||||
val subscriber = PrintingSubscriber(printerFun, out)
|
|
||||||
uncheckedCast(elements).subscribe(subscriber)
|
|
||||||
return Pair(subscriber, subscriber.future)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
package net.corda.tools.shell
|
|
||||||
|
|
||||||
import net.corda.core.messaging.RPCOps
|
|
||||||
import org.crsh.command.BaseCommand
|
|
||||||
import org.crsh.shell.impl.command.CRaSHSession
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simply extends CRaSH BaseCommand to add easy access to the RPC ops class.
|
|
||||||
*/
|
|
||||||
internal abstract class InteractiveShellCommand<T : RPCOps> : BaseCommand() {
|
|
||||||
|
|
||||||
abstract val rpcOpsClass: Class<out T>
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun ops(): T {
|
|
||||||
val cRaSHSession = context.session as CRaSHSession
|
|
||||||
val authInfo = cRaSHSession.authInfo as SshAuthInfo
|
|
||||||
return authInfo.getOrCreateRpcOps(rpcOpsClass)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ansiProgressRenderer() = ((context.session as CRaSHSession).authInfo as CordaSSHAuthInfo).ansiProgressRenderer
|
|
||||||
|
|
||||||
fun isSsh() = ((context.session as CRaSHSession).authInfo as CordaSSHAuthInfo).isSsh
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
package net.corda.tools.shell
|
|
||||||
|
|
||||||
import net.corda.client.rpc.CordaRPCClient
|
|
||||||
import net.corda.client.rpc.CordaRPCClientConfiguration
|
|
||||||
import net.corda.client.rpc.GracefulReconnect
|
|
||||||
import net.corda.client.rpc.RPCConnection
|
|
||||||
import net.corda.client.rpc.internal.RPCClient
|
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
|
||||||
import net.corda.core.messaging.RPCOps
|
|
||||||
|
|
||||||
internal interface RPCOpsProducer {
|
|
||||||
/**
|
|
||||||
* Returns [RPCConnection] of underlying proxy. Proxy can be obtained at any time by calling [RPCConnection.proxy]
|
|
||||||
*/
|
|
||||||
operator fun <T : RPCOps> invoke(username: String?, credential: String?, rpcOpsClass: Class<T>) : RPCConnection<T>
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class DefaultRPCOpsProducer(private val configuration: ShellConfiguration, private val classLoader: ClassLoader? = null, private val standalone: Boolean) : RPCOpsProducer {
|
|
||||||
|
|
||||||
override fun <T : RPCOps> invoke(username: String?, credential: String?, rpcOpsClass: Class<T>): RPCConnection<T> {
|
|
||||||
|
|
||||||
return if (rpcOpsClass == CordaRPCOps::class.java) {
|
|
||||||
// For CordaRPCOps we are using CordaRPCClient
|
|
||||||
val connection = if (standalone) {
|
|
||||||
CordaRPCClient(
|
|
||||||
configuration.hostAndPort,
|
|
||||||
configuration.ssl,
|
|
||||||
classLoader
|
|
||||||
).start(username!!, credential!!, gracefulReconnect = GracefulReconnect())
|
|
||||||
} else {
|
|
||||||
CordaRPCClient(
|
|
||||||
hostAndPort = configuration.hostAndPort,
|
|
||||||
configuration = CordaRPCClientConfiguration.DEFAULT.copy(
|
|
||||||
maxReconnectAttempts = 1
|
|
||||||
),
|
|
||||||
sslConfiguration = configuration.ssl,
|
|
||||||
classLoader = classLoader
|
|
||||||
).start(username!!, credential!!)
|
|
||||||
}
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
connection as RPCConnection<T>
|
|
||||||
} else {
|
|
||||||
// For other types "plain" RPCClient is used
|
|
||||||
val rpcClient = RPCClient<T>(configuration.hostAndPort, configuration.ssl)
|
|
||||||
val connection = rpcClient.start(rpcOpsClass, username!!, credential!!)
|
|
||||||
connection
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,93 +0,0 @@
|
|||||||
package net.corda.tools.shell
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonGenerator
|
|
||||||
import com.fasterxml.jackson.core.JsonParser
|
|
||||||
import com.fasterxml.jackson.databind.DeserializationContext
|
|
||||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
|
||||||
import com.fasterxml.jackson.databind.JsonSerializer
|
|
||||||
import com.fasterxml.jackson.databind.SerializerProvider
|
|
||||||
import com.google.common.io.Closeables
|
|
||||||
import net.corda.core.contracts.UniqueIdentifier
|
|
||||||
import net.corda.core.internal.copyTo
|
|
||||||
import net.corda.core.internal.inputStream
|
|
||||||
import org.crsh.command.InvocationContext
|
|
||||||
import rx.Observable
|
|
||||||
import java.io.BufferedInputStream
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.nio.file.Paths
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
//region Extra serializers
|
|
||||||
//
|
|
||||||
// These serializers are used to enable the user to specify objects that aren't natural data containers in the shell,
|
|
||||||
// and for the shell to print things out that otherwise wouldn't be usefully printable.
|
|
||||||
|
|
||||||
object ObservableSerializer : JsonSerializer<Observable<*>>() {
|
|
||||||
override fun serialize(value: Observable<*>, gen: JsonGenerator, serializers: SerializerProvider) {
|
|
||||||
gen.writeString("(observable)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* String value deserialized to [UniqueIdentifier].
|
|
||||||
* Any string value used as [UniqueIdentifier.externalId].
|
|
||||||
* If string contains underscore(i.e. externalId_uuid) then split with it.
|
|
||||||
* Index 0 as [UniqueIdentifier.externalId]
|
|
||||||
* Index 1 as [UniqueIdentifier.id]
|
|
||||||
* */
|
|
||||||
object UniqueIdentifierDeserializer : JsonDeserializer<UniqueIdentifier>() {
|
|
||||||
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): UniqueIdentifier {
|
|
||||||
//Check if externalId and UUID may be separated by underscore.
|
|
||||||
if (p.text.contains("_")) {
|
|
||||||
val ids = p.text.split("_")
|
|
||||||
//Create UUID object from string.
|
|
||||||
val uuid: UUID = UUID.fromString(ids[1])
|
|
||||||
//Create UniqueIdentifier object using externalId and UUID.
|
|
||||||
return UniqueIdentifier(ids[0], uuid)
|
|
||||||
}
|
|
||||||
//Any other string used as externalId.
|
|
||||||
return UniqueIdentifier.fromString(p.text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// An InputStream found in a response triggers a request to the user to provide somewhere to save it.
|
|
||||||
object InputStreamSerializer : JsonSerializer<InputStream>() {
|
|
||||||
var invokeContext: InvocationContext<*>? = null
|
|
||||||
|
|
||||||
override fun serialize(value: InputStream, gen: JsonGenerator, serializers: SerializerProvider) {
|
|
||||||
|
|
||||||
value.use {
|
|
||||||
val toPath = invokeContext!!.readLine("Path to save stream to (enter to ignore): ", true)
|
|
||||||
if (toPath == null || toPath.isBlank()) {
|
|
||||||
gen.writeString("<not saved>")
|
|
||||||
} else {
|
|
||||||
val path = Paths.get(toPath)
|
|
||||||
it.copyTo(path)
|
|
||||||
gen.writeString("<saved to: ${path.toAbsolutePath()}>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A file name is deserialized to an InputStream if found.
|
|
||||||
object InputStreamDeserializer : JsonDeserializer<InputStream>() {
|
|
||||||
// Keep track of them so we can close them later.
|
|
||||||
private val streams = Collections.synchronizedSet(HashSet<InputStream>())
|
|
||||||
|
|
||||||
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): InputStream {
|
|
||||||
val stream = object : BufferedInputStream(Paths.get(p.text).inputStream()) {
|
|
||||||
override fun close() {
|
|
||||||
super.close()
|
|
||||||
streams.remove(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
streams += stream
|
|
||||||
return stream
|
|
||||||
}
|
|
||||||
|
|
||||||
fun closeAll() {
|
|
||||||
// Clone the set with toList() here so each closed stream can be removed from the set inside close().
|
|
||||||
streams.toList().forEach { Closeables.closeQuietly(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//endregion
|
|
@ -1,25 +0,0 @@
|
|||||||
package net.corda.tools.shell
|
|
||||||
|
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
|
||||||
import net.corda.core.messaging.ClientRpcSslOptions
|
|
||||||
import java.nio.file.Path
|
|
||||||
|
|
||||||
data class ShellConfiguration(
|
|
||||||
val commandsDirectory: Path,
|
|
||||||
val cordappsDirectory: Path? = null,
|
|
||||||
var user: String = "",
|
|
||||||
var password: String = "",
|
|
||||||
var permissions: Set<String>? = null,
|
|
||||||
var localShellAllowExitInSafeMode: Boolean = false,
|
|
||||||
var localShellUnsafe: Boolean = false,
|
|
||||||
val hostAndPort: NetworkHostAndPort,
|
|
||||||
val ssl: ClientRpcSslOptions? = null,
|
|
||||||
val sshdPort: Int? = null,
|
|
||||||
val sshHostKeyDirectory: Path? = null,
|
|
||||||
val noLocalShell: Boolean = false) {
|
|
||||||
companion object {
|
|
||||||
const val COMMANDS_DIR = "shell-commands"
|
|
||||||
const val CORDAPPS_DIR = "cordapps"
|
|
||||||
const val SSHD_HOSTKEY_DIR = "ssh"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,356 +0,0 @@
|
|||||||
package net.corda.tools.shell.utlities
|
|
||||||
|
|
||||||
import net.corda.core.internal.Emoji
|
|
||||||
import net.corda.core.messaging.FlowProgressHandle
|
|
||||||
import net.corda.core.utilities.loggerFor
|
|
||||||
import net.corda.tools.shell.utlities.StdoutANSIProgressRenderer.draw
|
|
||||||
import org.apache.commons.lang3.SystemUtils
|
|
||||||
import org.apache.logging.log4j.LogManager
|
|
||||||
import org.apache.logging.log4j.core.LogEvent
|
|
||||||
import org.apache.logging.log4j.core.LoggerContext
|
|
||||||
import org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender
|
|
||||||
import org.apache.logging.log4j.core.appender.ConsoleAppender
|
|
||||||
import org.apache.logging.log4j.core.appender.OutputStreamManager
|
|
||||||
import org.crsh.text.RenderPrintWriter
|
|
||||||
import org.fusesource.jansi.Ansi
|
|
||||||
import org.fusesource.jansi.Ansi.Attribute
|
|
||||||
import org.fusesource.jansi.AnsiConsole
|
|
||||||
import org.fusesource.jansi.AnsiOutputStream
|
|
||||||
import rx.Observable.combineLatest
|
|
||||||
import rx.Subscription
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
abstract class ANSIProgressRenderer {
|
|
||||||
|
|
||||||
private var updatesSubscription: Subscription? = null
|
|
||||||
|
|
||||||
protected var usingANSI = false
|
|
||||||
protected var checkEmoji = false
|
|
||||||
private val usingUnicode = !SystemUtils.IS_OS_WINDOWS
|
|
||||||
|
|
||||||
private var treeIndex: Int = 0
|
|
||||||
private var treeIndexProcessed: MutableSet<Int> = mutableSetOf()
|
|
||||||
protected var tree: List<ProgressStep> = listOf()
|
|
||||||
|
|
||||||
private var installedYet = false
|
|
||||||
|
|
||||||
private var onDone: () -> Unit = {}
|
|
||||||
|
|
||||||
// prevMessagePrinted is just for non-ANSI mode.
|
|
||||||
private var prevMessagePrinted: String? = null
|
|
||||||
// prevLinesDraw is just for ANSI mode.
|
|
||||||
protected var prevLinesDrawn = 0
|
|
||||||
|
|
||||||
data class ProgressStep(val level: Int, val description: String, val parentIndex: Int?)
|
|
||||||
data class InputTreeStep(val level: Int, val description: String)
|
|
||||||
|
|
||||||
private fun done(error: Throwable?) {
|
|
||||||
if (error == null) renderInternal(null)
|
|
||||||
draw(true, error)
|
|
||||||
onDone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun render(flowProgressHandle: FlowProgressHandle<*>, onDone: () -> Unit = {}) {
|
|
||||||
this.onDone = onDone
|
|
||||||
renderInternal(flowProgressHandle)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract fun printLine(line:String)
|
|
||||||
|
|
||||||
protected abstract fun printAnsi(ansi:Ansi)
|
|
||||||
|
|
||||||
protected abstract fun setup()
|
|
||||||
|
|
||||||
private fun renderInternal(flowProgressHandle: FlowProgressHandle<*>?) {
|
|
||||||
updatesSubscription?.unsubscribe()
|
|
||||||
treeIndex = 0
|
|
||||||
treeIndexProcessed.clear()
|
|
||||||
tree = listOf()
|
|
||||||
|
|
||||||
if (!installedYet) {
|
|
||||||
setup()
|
|
||||||
installedYet = true
|
|
||||||
}
|
|
||||||
|
|
||||||
prevMessagePrinted = null
|
|
||||||
prevLinesDrawn = 0
|
|
||||||
draw(true)
|
|
||||||
|
|
||||||
val treeUpdates = flowProgressHandle?.stepsTreeFeed?.updates
|
|
||||||
val indexUpdates = flowProgressHandle?.stepsTreeIndexFeed?.updates
|
|
||||||
|
|
||||||
if (treeUpdates == null || indexUpdates == null) {
|
|
||||||
renderInBold("Cannot print progress for this flow as the required data is missing", Ansi())
|
|
||||||
} else {
|
|
||||||
// By combining the two observables, a race condition where both emit items at roughly the same time is avoided. This could
|
|
||||||
// result in steps being incorrectly marked as skipped. Instead, whenever either observable emits an item, a pair of the
|
|
||||||
// last index and last tree is returned, which ensures that updates to either are processed in series.
|
|
||||||
updatesSubscription = combineLatest(treeUpdates, indexUpdates) { tree, index -> Pair(tree, index) }.subscribe(
|
|
||||||
{
|
|
||||||
val newTree = transformTree(it.first.map { elem -> InputTreeStep(elem.first, elem.second) })
|
|
||||||
// Process indices first, as if the tree has changed the associated index with this update is for the old tree. Note
|
|
||||||
// that the one case where this isn't true is the very first update, but in this case the index should be 0 (as this
|
|
||||||
// update is for the initial state). The remapping on a new tree assumes the step at index 0 is always at least current,
|
|
||||||
// so this case is handled there.
|
|
||||||
treeIndex = it.second
|
|
||||||
treeIndexProcessed.add(it.second)
|
|
||||||
if (newTree != tree) {
|
|
||||||
remapIndices(newTree)
|
|
||||||
tree = newTree
|
|
||||||
}
|
|
||||||
draw(true)
|
|
||||||
},
|
|
||||||
{ done(it) },
|
|
||||||
{ done(null) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new tree of steps that also holds a reference to the parent of each step. This is required to uniquely identify each step
|
|
||||||
// (assuming that each step label is unique at a given level).
|
|
||||||
private fun transformTree(inputTree: List<InputTreeStep>): List<ProgressStep> {
|
|
||||||
if (inputTree.isEmpty()) {
|
|
||||||
return listOf()
|
|
||||||
}
|
|
||||||
val stack = Stack<Pair<Int, InputTreeStep>>()
|
|
||||||
stack.push(Pair(0, inputTree[0]))
|
|
||||||
return inputTree.mapIndexed { index, step ->
|
|
||||||
val parentIndex = try {
|
|
||||||
val top = stack.peek()
|
|
||||||
val levelDifference = top.second.level - step.level
|
|
||||||
if (levelDifference >= 0) {
|
|
||||||
// The top of the stack is at the same or lower level than the current step. Remove items from the top until the topmost
|
|
||||||
// item is at a higher level - this is the parent step.
|
|
||||||
repeat(levelDifference + 1) { stack.pop() }
|
|
||||||
}
|
|
||||||
stack.peek().first
|
|
||||||
} catch (e: EmptyStackException) {
|
|
||||||
// If there is nothing on the stack at any point, it implies that this step is at the top level and has no parent.
|
|
||||||
null
|
|
||||||
}
|
|
||||||
stack.push(Pair(index, step))
|
|
||||||
ProgressStep(step.level, step.description, parentIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun remapIndices(newTree: List<ProgressStep>) {
|
|
||||||
val newIndices = newTree.filter {
|
|
||||||
treeIndexProcessed.contains(tree.indexOf(it))
|
|
||||||
}.map {
|
|
||||||
newTree.indexOf(it)
|
|
||||||
}.toMutableSet()
|
|
||||||
treeIndex = newIndices.max() ?: 0
|
|
||||||
treeIndexProcessed = if (newIndices.isNotEmpty()) newIndices else mutableSetOf(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized protected fun draw(moveUp: Boolean, error: Throwable? = null) {
|
|
||||||
|
|
||||||
if (!usingANSI) {
|
|
||||||
val currentMessage = tree.getOrNull(treeIndex)?.description
|
|
||||||
if (currentMessage != null && currentMessage != prevMessagePrinted) {
|
|
||||||
printLine(currentMessage)
|
|
||||||
prevMessagePrinted = currentMessage
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fun printingBody() {
|
|
||||||
// Handle the case where the number of steps in a progress tracker is changed during execution.
|
|
||||||
val ansi = Ansi()
|
|
||||||
if (prevLinesDrawn > 0 && moveUp)
|
|
||||||
ansi.cursorUp(prevLinesDrawn)
|
|
||||||
|
|
||||||
// Put a blank line between any logging and us.
|
|
||||||
ansi.eraseLine()
|
|
||||||
ansi.newline()
|
|
||||||
if (tree.isEmpty()) return
|
|
||||||
var newLinesDrawn = 1 + renderLevel(ansi, error != null)
|
|
||||||
|
|
||||||
if (error != null) {
|
|
||||||
val errorIcon = if (usingUnicode) Emoji.skullAndCrossbones else "ERROR: "
|
|
||||||
|
|
||||||
var errorToPrint = error
|
|
||||||
var indent = 0
|
|
||||||
while (errorToPrint != null) {
|
|
||||||
ansi.fgRed()
|
|
||||||
ansi.a("${"\t".repeat(indent)}$errorIcon ${errorToPrint.message}")
|
|
||||||
ansi.newline()
|
|
||||||
errorToPrint = errorToPrint.cause
|
|
||||||
indent++
|
|
||||||
}
|
|
||||||
ansi.reset()
|
|
||||||
|
|
||||||
ansi.eraseLine(Ansi.Erase.FORWARD)
|
|
||||||
ansi.newline()
|
|
||||||
newLinesDrawn++
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newLinesDrawn < prevLinesDrawn) {
|
|
||||||
// If some steps were removed from the progress tracker, we don't want to leave junk hanging around below.
|
|
||||||
val linesToClear = prevLinesDrawn - newLinesDrawn
|
|
||||||
repeat(linesToClear) {
|
|
||||||
ansi.eraseLine()
|
|
||||||
ansi.newline()
|
|
||||||
}
|
|
||||||
ansi.cursorUp(linesToClear)
|
|
||||||
}
|
|
||||||
prevLinesDrawn = newLinesDrawn
|
|
||||||
|
|
||||||
printAnsi(ansi)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkEmoji) {
|
|
||||||
Emoji.renderIfSupported(::printingBody)
|
|
||||||
} else {
|
|
||||||
printingBody()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns number of lines rendered.
|
|
||||||
private fun renderLevel(ansi: Ansi, error: Boolean): Int {
|
|
||||||
with(ansi) {
|
|
||||||
var lines = 0
|
|
||||||
for ((index, step) in tree.withIndex()) {
|
|
||||||
val processedStep = treeIndexProcessed.contains(index)
|
|
||||||
val skippedStep = index < treeIndex && !processedStep
|
|
||||||
val activeStep = index == treeIndex
|
|
||||||
|
|
||||||
val marker = when {
|
|
||||||
activeStep -> if (usingUnicode) "${Emoji.rightArrow} " else "CURRENT: "
|
|
||||||
processedStep -> if (usingUnicode) " ${Emoji.greenTick} " else "DONE: "
|
|
||||||
skippedStep -> " "
|
|
||||||
error -> if (usingUnicode) "${Emoji.noEntry} " else "ERROR: "
|
|
||||||
else -> " " // Not reached yet.
|
|
||||||
}
|
|
||||||
a(" ".repeat(step.level))
|
|
||||||
a(marker)
|
|
||||||
|
|
||||||
when {
|
|
||||||
activeStep -> renderInBold(step.description, ansi)
|
|
||||||
skippedStep -> renderInFaint(step.description, ansi)
|
|
||||||
else -> a(step.description)
|
|
||||||
}
|
|
||||||
|
|
||||||
eraseLine(Ansi.Erase.FORWARD)
|
|
||||||
newline()
|
|
||||||
lines++
|
|
||||||
}
|
|
||||||
return lines
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun renderInBold(payload: String, ansi: Ansi) {
|
|
||||||
with(ansi) {
|
|
||||||
a(Attribute.INTENSITY_BOLD)
|
|
||||||
a(payload)
|
|
||||||
a(Attribute.INTENSITY_BOLD_OFF)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun renderInFaint(payload: String, ansi: Ansi) {
|
|
||||||
with(ansi) {
|
|
||||||
a(Attribute.INTENSITY_FAINT)
|
|
||||||
a(payload)
|
|
||||||
a(Attribute.INTENSITY_BOLD_OFF)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class CRaSHANSIProgressRenderer(val renderPrintWriter:RenderPrintWriter) : ANSIProgressRenderer() {
|
|
||||||
|
|
||||||
override fun printLine(line: String) {
|
|
||||||
renderPrintWriter.println(line)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun printAnsi(ansi: Ansi) {
|
|
||||||
renderPrintWriter.print(ansi)
|
|
||||||
renderPrintWriter.flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setup() {
|
|
||||||
// We assume SSH always use ANSI.
|
|
||||||
usingANSI = true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Knows how to render a [FlowProgressHandle] to the terminal using coloured, emoji-fied output. Useful when writing small
|
|
||||||
* command line tools, demos, tests etc. Just call [draw] method and it will go ahead and start drawing
|
|
||||||
* if the terminal supports it. Otherwise it just prints out the name of the step whenever it changes.
|
|
||||||
*
|
|
||||||
* When a progress tracker is on the screen, it takes over the bottom part and reconfigures logging so that, assuming
|
|
||||||
* 1 log event == 1 line, the progress tracker is always glued to the bottom and logging scrolls above it.
|
|
||||||
*
|
|
||||||
* TODO: More thread safety
|
|
||||||
*/
|
|
||||||
object StdoutANSIProgressRenderer : ANSIProgressRenderer() {
|
|
||||||
|
|
||||||
override fun setup() {
|
|
||||||
AnsiConsole.systemInstall()
|
|
||||||
checkEmoji = true
|
|
||||||
|
|
||||||
// This line looks weird as hell because the magic code to decide if we really have a TTY or not isn't
|
|
||||||
// actually exposed anywhere as a function (weak sauce). So we have to rely on our knowledge of jansi
|
|
||||||
// implementation details.
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
usingANSI = AnsiConsole.wrapOutputStream(System.out) !is AnsiOutputStream
|
|
||||||
|
|
||||||
if (usingANSI) {
|
|
||||||
// This super ugly code hacks into log4j and swaps out its console appender for our own. It's a bit simpler
|
|
||||||
// than doing things the official way with a dedicated plugin, etc, as it avoids mucking around with all
|
|
||||||
// the config XML and lifecycle goop.
|
|
||||||
val manager = LogManager.getContext(false) as LoggerContext
|
|
||||||
val consoleAppender = manager.configuration.appenders.values.filterIsInstance<ConsoleAppender>().singleOrNull { it.name == "Console-Selector" }
|
|
||||||
if (consoleAppender == null) {
|
|
||||||
loggerFor<StdoutANSIProgressRenderer>().warn("Cannot find console appender - progress tracking may not work as expected")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val scrollingAppender = object : AbstractOutputStreamAppender<OutputStreamManager>(
|
|
||||||
consoleAppender.name, consoleAppender.layout, consoleAppender.filter,
|
|
||||||
consoleAppender.ignoreExceptions(), true, consoleAppender.manager) {
|
|
||||||
override fun append(event: LogEvent) {
|
|
||||||
// We lock on the renderer to avoid threads that are logging to the screen simultaneously messing
|
|
||||||
// things up. Of course this slows stuff down a bit, but only whilst this little utility is in use.
|
|
||||||
// Eventually it will be replaced with a real GUI and we can delete all this.
|
|
||||||
synchronized(StdoutANSIProgressRenderer) {
|
|
||||||
if (tree.isNotEmpty()) {
|
|
||||||
val ansi = Ansi.ansi()
|
|
||||||
repeat(prevLinesDrawn) { ansi.eraseLine().cursorUp(1).eraseLine() }
|
|
||||||
System.out.print(ansi)
|
|
||||||
System.out.flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
super.append(event)
|
|
||||||
|
|
||||||
if (tree.isNotEmpty())
|
|
||||||
draw(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scrollingAppender.start()
|
|
||||||
manager.configuration.appenders[consoleAppender.name] = scrollingAppender
|
|
||||||
val loggerConfigs = manager.configuration.loggers.values
|
|
||||||
for (config in loggerConfigs) {
|
|
||||||
val appenderRefs = config.appenderRefs
|
|
||||||
val consoleAppenders = config.appenders.filter { it.value is ConsoleAppender }.keys
|
|
||||||
consoleAppenders.forEach { config.removeAppender(it) }
|
|
||||||
appenderRefs.forEach { config.addAppender(manager.configuration.appenders[it.ref], it.level, it.filter) }
|
|
||||||
}
|
|
||||||
manager.updateLoggers()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun printLine(line:String) {
|
|
||||||
System.out.println(line)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun printAnsi(ansi: Ansi) {
|
|
||||||
// Need to force a flush here in order to ensure stderr/stdout sync up properly.
|
|
||||||
System.out.print(ansi)
|
|
||||||
System.out.flush()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
package net.corda.tools.shell.base
|
|
||||||
|
|
||||||
// Note that this file MUST be in a sub-directory called "base" relative to the path
|
|
||||||
// given in the configuration code in InteractiveShell.
|
|
||||||
|
|
||||||
welcome = { ->
|
|
||||||
"""
|
|
||||||
|
|
||||||
Welcome to the Corda interactive shell.
|
|
||||||
You can see the available commands by typing 'help'.
|
|
||||||
|
|
||||||
""".stripIndent()
|
|
||||||
}
|
|
||||||
|
|
||||||
prompt = { ->
|
|
||||||
return "${new Date()}>>> "
|
|
||||||
}
|
|
@ -1,279 +0,0 @@
|
|||||||
package net.corda.tools.shell;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
import kotlin.Pair;
|
|
||||||
import net.corda.client.jackson.JacksonSupport;
|
|
||||||
import net.corda.client.jackson.internal.ToStringSerialize;
|
|
||||||
import net.corda.core.contracts.Amount;
|
|
||||||
import net.corda.core.crypto.SecureHash;
|
|
||||||
import net.corda.core.flows.FlowException;
|
|
||||||
import net.corda.core.flows.FlowLogic;
|
|
||||||
import net.corda.core.flows.FlowSession;
|
|
||||||
import net.corda.core.flows.StateMachineRunId;
|
|
||||||
import net.corda.core.identity.CordaX500Name;
|
|
||||||
import net.corda.core.identity.Party;
|
|
||||||
import net.corda.core.internal.concurrent.CordaFutureImplKt;
|
|
||||||
import net.corda.core.internal.concurrent.OpenFuture;
|
|
||||||
import net.corda.core.messaging.FlowProgressHandleImpl;
|
|
||||||
import net.corda.core.utilities.ProgressTracker;
|
|
||||||
import net.corda.coretesting.internal.InternalTestConstantsKt;
|
|
||||||
import net.corda.node.services.identity.InMemoryIdentityService;
|
|
||||||
import net.corda.testing.core.TestIdentity;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
import org.junit.Test;
|
|
||||||
import rx.Observable;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import static java.util.stream.Collectors.toList;
|
|
||||||
import static org.junit.Assert.assertEquals;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
|
||||||
|
|
||||||
public class InteractiveShellJavaTest {
|
|
||||||
private static TestIdentity megaCorp = new TestIdentity(new CordaX500Name("MegaCorp", "London", "GB"));
|
|
||||||
|
|
||||||
// should guarantee that FlowA will have synthetic method to access this field
|
|
||||||
private static final String synthetic = "synth";
|
|
||||||
|
|
||||||
private static final boolean IS_OPENJ9 = System.getProperty("java.vm.name").toLowerCase().contains("openj9");
|
|
||||||
|
|
||||||
abstract static class StringFlow extends FlowLogic<String> {
|
|
||||||
abstract String getA();
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public static class FlowA extends StringFlow {
|
|
||||||
|
|
||||||
private String a;
|
|
||||||
|
|
||||||
public FlowA(String a) {
|
|
||||||
if (!synthetic.isEmpty()) {
|
|
||||||
this.a = a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public FlowA(int b) {
|
|
||||||
this(Integer.valueOf(b).toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public FlowA(Integer b, String c) {
|
|
||||||
this(b.toString() + c);
|
|
||||||
}
|
|
||||||
|
|
||||||
public FlowA(Amount<Currency> amount) {
|
|
||||||
this(amount.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public FlowA(Pair<Amount<Currency>, SecureHash.SHA256> pair) {
|
|
||||||
this(pair.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public FlowA(Party party) {
|
|
||||||
this(party.getName().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public FlowA(Integer b, Amount<UserValue> amount) {
|
|
||||||
this(String.format("%d %s", amount.getQuantity() + (b == null ? 0 : b), amount.getToken()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public FlowA(String[] b) {
|
|
||||||
this(String.join("+", b));
|
|
||||||
}
|
|
||||||
|
|
||||||
public FlowA(Amount<UserValue>[] amounts) {
|
|
||||||
this(String.join("++", Arrays.stream(amounts).map(Amount::toString).collect(toList())));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public ProgressTracker getProgressTracker() {
|
|
||||||
return new ProgressTracker();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String call() {
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
String getA() {
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class FlowB extends StringFlow {
|
|
||||||
|
|
||||||
private Party party;
|
|
||||||
private String a;
|
|
||||||
|
|
||||||
public FlowB(Party party, String a) {
|
|
||||||
this.party = party;
|
|
||||||
this.a = a;
|
|
||||||
}
|
|
||||||
|
|
||||||
public FlowB(Amount<Currency> amount, int abc) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public ProgressTracker getProgressTracker() {
|
|
||||||
return new ProgressTracker();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String call() throws FlowException {
|
|
||||||
FlowSession session = initiateFlow(party);
|
|
||||||
|
|
||||||
|
|
||||||
Integer integer = session.receive(Integer.class).unwrap((i) -> i);
|
|
||||||
|
|
||||||
return integer.toString();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
String getA() {
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ToStringSerialize
|
|
||||||
public static class UserValue {
|
|
||||||
private final String label;
|
|
||||||
|
|
||||||
public UserValue(@JsonProperty("label") String label) {
|
|
||||||
this.label = label;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unused") // Used via reflection.
|
|
||||||
public String getLabel() {
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private InMemoryIdentityService ids = new InMemoryIdentityService(Lists.newArrayList(megaCorp.getIdentity()), InternalTestConstantsKt.getDEV_ROOT_CA().getCertificate());
|
|
||||||
|
|
||||||
private ObjectMapper om = JacksonSupport.createInMemoryMapper(ids, new YAMLFactory());
|
|
||||||
|
|
||||||
private String output;
|
|
||||||
|
|
||||||
private void check(String input, String expected, Class<? extends StringFlow> flowClass) throws InteractiveShell.NoApplicableConstructor {
|
|
||||||
InteractiveShell.INSTANCE.runFlowFromString((clazz, args) -> {
|
|
||||||
StringFlow instance = null;
|
|
||||||
try {
|
|
||||||
instance = (StringFlow)clazz.getConstructor(Arrays.stream(args).map(Object::getClass).toArray(Class[]::new)).newInstance(args);
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println(e);
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
output = instance.getA();
|
|
||||||
OpenFuture<String> future = CordaFutureImplKt.openFuture();
|
|
||||||
future.set("ABC");
|
|
||||||
return new FlowProgressHandleImpl<String>(StateMachineRunId.Companion.createRandom(), future, Observable.just("Some string"));
|
|
||||||
}, input, flowClass, om);
|
|
||||||
assertEquals(input, expected, output);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void flowStartSimple() throws InteractiveShell.NoApplicableConstructor {
|
|
||||||
check("a: Hi there", "Hi there", FlowA.class);
|
|
||||||
if (!IS_OPENJ9) {
|
|
||||||
check("b: 12", "12", FlowA.class);
|
|
||||||
check("b: 12, c: Yo", "12Yo", FlowA.class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void flowStartWithComplexTypes() throws InteractiveShell.NoApplicableConstructor {
|
|
||||||
check("amount: £10", "10.00 GBP", FlowA.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void flowStartWithNestedTypes() throws InteractiveShell.NoApplicableConstructor {
|
|
||||||
check(
|
|
||||||
"pair: { first: $100.12, second: df489807f81c8c8829e509e1bcb92e6692b9dd9d624b7456435cb2f51dc82587 }",
|
|
||||||
"(100.12 USD, DF489807F81C8C8829E509E1BCB92E6692B9DD9D624B7456435CB2F51DC82587)",
|
|
||||||
FlowA.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void flowStartWithUserAmount() throws InteractiveShell.NoApplicableConstructor {
|
|
||||||
check(
|
|
||||||
"b: 500, amount: { \"quantity\": 10001, \"token\":{ \"label\": \"of value\" } }",
|
|
||||||
"10501 of value",
|
|
||||||
FlowA.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void flowStartWithArrayType() throws InteractiveShell.NoApplicableConstructor {
|
|
||||||
if (!IS_OPENJ9) {
|
|
||||||
check(
|
|
||||||
"b: [ One, Two, Three, Four ]",
|
|
||||||
"One+Two+Three+Four",
|
|
||||||
FlowA.class
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void flowStartWithArrayOfNestedType() throws InteractiveShell.NoApplicableConstructor {
|
|
||||||
check(
|
|
||||||
"amounts: [ { \"quantity\": 10, \"token\": { \"label\": \"(1)\" } }, { \"quantity\": 200, \"token\": { \"label\": \"(2)\" } } ]",
|
|
||||||
"10 (1)++200 (2)",
|
|
||||||
FlowA.class
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(expected = InteractiveShell.NoApplicableConstructor.class)
|
|
||||||
public void flowStartNoArgs() throws InteractiveShell.NoApplicableConstructor {
|
|
||||||
check("", "", FlowA.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(expected = InteractiveShell.NoApplicableConstructor.class)
|
|
||||||
public void flowMissingParam() throws InteractiveShell.NoApplicableConstructor {
|
|
||||||
check("c: Yo", "", FlowA.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(expected = InteractiveShell.NoApplicableConstructor.class)
|
|
||||||
public void flowTooManyParams() throws InteractiveShell.NoApplicableConstructor {
|
|
||||||
check("b: 12, c: Yo, d: Bar", "", FlowA.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void party() throws InteractiveShell.NoApplicableConstructor {
|
|
||||||
check("party: \"" + megaCorp.getName() + "\"", megaCorp.getName().toString(), FlowA.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void unwrapLambda() throws InteractiveShell.NoApplicableConstructor {
|
|
||||||
check("party: \"" + megaCorp.getName() + "\", a: Bambam", "Bambam", FlowB.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void niceErrors() {
|
|
||||||
// Most cases are checked in the Kotlin test, so we only check raw types here.
|
|
||||||
try {
|
|
||||||
check("amount: $100", "", FlowB.class);
|
|
||||||
} catch (InteractiveShell.NoApplicableConstructor e) {
|
|
||||||
assertEquals("[amount: Amount<Currency>, abc: int]: missing parameter abc", e.getErrors().get(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void flowStartWithUnknownParty() throws InteractiveShell.NoApplicableConstructor {
|
|
||||||
try {
|
|
||||||
check("party: nonexistent", "", FlowA.class);
|
|
||||||
} catch (InteractiveShell.NoApplicableConstructor e) {
|
|
||||||
assertTrue(e.getErrors().get(0).contains("No matching Party found"));
|
|
||||||
assertEquals(1, e.getErrors().size());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
package net.corda.tools.shell;
|
|
||||||
|
|
||||||
import org.crsh.command.InvocationContext;
|
|
||||||
import org.crsh.command.ScriptException;
|
|
||||||
import org.crsh.text.RenderPrintWriter;
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
import static org.mockito.Mockito.verify;
|
|
||||||
|
|
||||||
public class OutputFormatCommandTest {
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private InvocationContext<Map> mockInvocationContext = mock(InvocationContext.class);
|
|
||||||
private RenderPrintWriter printWriter;
|
|
||||||
|
|
||||||
private OutputFormatCommand outputFormatCommand;
|
|
||||||
|
|
||||||
private static final String JSON_FORMAT_STRING = "json";
|
|
||||||
private static final String YAML_FORMAT_STRING = "yaml";
|
|
||||||
|
|
||||||
@Before
|
|
||||||
public void setup() {
|
|
||||||
printWriter = mock(RenderPrintWriter.class);
|
|
||||||
outputFormatCommand = new OutputFormatCommand(printWriter);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testValidUpdateToJson() {
|
|
||||||
outputFormatCommand.set(mockInvocationContext, JSON_FORMAT_STRING);
|
|
||||||
outputFormatCommand.get(mockInvocationContext);
|
|
||||||
|
|
||||||
verify(printWriter).println(JSON_FORMAT_STRING);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testValidUpdateToYaml() {
|
|
||||||
outputFormatCommand.set(mockInvocationContext, YAML_FORMAT_STRING);
|
|
||||||
outputFormatCommand.get(mockInvocationContext);
|
|
||||||
|
|
||||||
verify(printWriter).println(YAML_FORMAT_STRING);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testInvalidUpdate() {
|
|
||||||
assertThatExceptionOfType(ScriptException.class).isThrownBy(() -> outputFormatCommand.set(mockInvocationContext, "some-invalid-format"))
|
|
||||||
.withMessage("The provided format is not supported: some-invalid-format");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
package net.corda.tools.shell
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonMappingException
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
|
||||||
import net.corda.core.contracts.UniqueIdentifier
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
|
|
||||||
class CustomTypeJsonParsingTests {
|
|
||||||
lateinit var objectMapper: ObjectMapper
|
|
||||||
|
|
||||||
//Dummy classes for testing.
|
|
||||||
data class State(val linearId: UniqueIdentifier) {
|
|
||||||
constructor() : this(UniqueIdentifier("required-for-json-deserializer"))
|
|
||||||
}
|
|
||||||
|
|
||||||
data class UuidState(val uuid: UUID) {
|
|
||||||
//Default constructor required for json deserializer.
|
|
||||||
constructor() : this(UUID.randomUUID())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
objectMapper = ObjectMapper()
|
|
||||||
val simpleModule = SimpleModule()
|
|
||||||
simpleModule.addDeserializer(UniqueIdentifier::class.java, UniqueIdentifierDeserializer)
|
|
||||||
objectMapper.registerModule(simpleModule)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `Deserializing UniqueIdentifier by parsing string`() {
|
|
||||||
val id = "26b37265-a1fd-4c77-b2e0-715917ef619f"
|
|
||||||
val json = """{"linearId":"$id"}"""
|
|
||||||
val state = objectMapper.readValue<State>(json)
|
|
||||||
|
|
||||||
assertEquals(id, state.linearId.id.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `Deserializing UniqueIdentifier by parsing string with underscore`() {
|
|
||||||
val json = """{"linearId":"extkey564_26b37265-a1fd-4c77-b2e0-715917ef619f"}"""
|
|
||||||
val state = objectMapper.readValue<State>(json)
|
|
||||||
|
|
||||||
assertEquals("extkey564", state.linearId.externalId)
|
|
||||||
assertEquals("26b37265-a1fd-4c77-b2e0-715917ef619f", state.linearId.id.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(expected = JsonMappingException::class, timeout=300_000)
|
|
||||||
fun `Deserializing by parsing string contain invalid uuid with underscore`() {
|
|
||||||
val json = """{"linearId":"extkey564_26b37265-a1fd-4c77-b2e0"}"""
|
|
||||||
objectMapper.readValue<State>(json)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `Deserializing UUID by parsing string`() {
|
|
||||||
val json = """{"uuid":"26b37265-a1fd-4c77-b2e0-715917ef619f"}"""
|
|
||||||
val state = objectMapper.readValue<UuidState>(json)
|
|
||||||
|
|
||||||
assertEquals("26b37265-a1fd-4c77-b2e0-715917ef619f", state.uuid.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(expected = JsonMappingException::class, timeout=300_000)
|
|
||||||
fun `Deserializing UUID by parsing invalid uuid string`() {
|
|
||||||
val json = """{"uuid":"26b37265-a1fd-4c77-b2e0"}"""
|
|
||||||
objectMapper.readValue<UuidState>(json)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
package net.corda.tools.shell
|
|
||||||
|
|
||||||
import net.corda.core.crypto.SecureHash
|
|
||||||
import net.corda.core.crypto.sha256
|
|
||||||
import net.corda.core.flows.StateMachineRunId
|
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
|
||||||
import net.corda.core.messaging.StateMachineTransactionMapping
|
|
||||||
import org.hamcrest.MatcherAssert
|
|
||||||
import org.hamcrest.core.StringContains
|
|
||||||
import org.junit.Test
|
|
||||||
import org.mockito.Mockito
|
|
||||||
import java.io.CharArrayWriter
|
|
||||||
import java.io.PrintWriter
|
|
||||||
import java.util.UUID
|
|
||||||
import kotlin.test.assertFailsWith
|
|
||||||
|
|
||||||
class HashLookupCommandTest {
|
|
||||||
companion object {
|
|
||||||
private val DEFAULT_TXID: SecureHash = SecureHash.randomSHA256()
|
|
||||||
|
|
||||||
private fun ops(vararg txIds: SecureHash): CordaRPCOps? {
|
|
||||||
val snapshot: List<StateMachineTransactionMapping> = txIds.map { txId ->
|
|
||||||
StateMachineTransactionMapping(StateMachineRunId(UUID.randomUUID()), txId)
|
|
||||||
}
|
|
||||||
return Mockito.mock(CordaRPCOps::class.java).apply {
|
|
||||||
Mockito.`when`(stateMachineRecordedTransactionMappingSnapshot()).thenReturn(snapshot)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun runCommand(ops: CordaRPCOps?, txIdHash: String): String {
|
|
||||||
val arrayWriter = CharArrayWriter()
|
|
||||||
return PrintWriter(arrayWriter).use {
|
|
||||||
HashLookupShellCommand.hashLookup(it, ops, txIdHash)
|
|
||||||
it.flush()
|
|
||||||
arrayWriter.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `hash lookup command returns correct response`() {
|
|
||||||
val ops = ops(DEFAULT_TXID)
|
|
||||||
var response = runCommand(ops, DEFAULT_TXID.toString())
|
|
||||||
|
|
||||||
MatcherAssert.assertThat(response, StringContains.containsString("Found a matching transaction with Id: $DEFAULT_TXID"))
|
|
||||||
|
|
||||||
// Verify the hash of the TX ID also works
|
|
||||||
response = runCommand(ops, DEFAULT_TXID.sha256().toString())
|
|
||||||
MatcherAssert.assertThat(response, StringContains.containsString("Found a matching transaction with Id: $DEFAULT_TXID"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `should reject invalid txid`() {
|
|
||||||
val ops = ops(DEFAULT_TXID)
|
|
||||||
assertFailsWith<IllegalArgumentException>("The provided string is not a valid hexadecimal SHA-256 hash value") {
|
|
||||||
runCommand(ops, "abcdefgh")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `should reject unknown txid`() {
|
|
||||||
val ops = ops(DEFAULT_TXID)
|
|
||||||
assertFailsWith<IllegalArgumentException>("No matching transaction found") {
|
|
||||||
runCommand(ops, SecureHash.randomSHA256().toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,284 +0,0 @@
|
|||||||
package net.corda.tools.shell
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import com.fasterxml.jackson.databind.type.TypeFactory
|
|
||||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
|
|
||||||
import com.nhaarman.mockito_kotlin.any
|
|
||||||
import com.nhaarman.mockito_kotlin.mock
|
|
||||||
import com.nhaarman.mockito_kotlin.verify
|
|
||||||
import com.nhaarman.mockito_kotlin.whenever
|
|
||||||
import net.corda.client.jackson.JacksonSupport
|
|
||||||
import net.corda.client.jackson.internal.ToStringSerialize
|
|
||||||
import net.corda.core.contracts.Amount
|
|
||||||
import net.corda.core.crypto.SecureHash
|
|
||||||
import net.corda.core.crypto.generateKeyPair
|
|
||||||
import net.corda.core.flows.FlowLogic
|
|
||||||
import net.corda.core.flows.StateMachineRunId
|
|
||||||
import net.corda.core.identity.CordaX500Name
|
|
||||||
import net.corda.core.identity.Party
|
|
||||||
import net.corda.core.internal.concurrent.openFuture
|
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
|
||||||
import net.corda.core.messaging.FlowProgressHandleImpl
|
|
||||||
import net.corda.core.node.NodeInfo
|
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
|
||||||
import net.corda.core.utilities.ProgressTracker
|
|
||||||
import net.corda.coretesting.internal.DEV_ROOT_CA
|
|
||||||
import net.corda.node.services.identity.InMemoryIdentityService
|
|
||||||
import net.corda.testing.core.ALICE_NAME
|
|
||||||
import net.corda.testing.core.BOB_NAME
|
|
||||||
import net.corda.testing.core.TestIdentity
|
|
||||||
import net.corda.testing.core.getTestPartyAndCertificate
|
|
||||||
import org.crsh.command.InvocationContext
|
|
||||||
import org.crsh.text.Color
|
|
||||||
import org.crsh.text.Decoration
|
|
||||||
import org.crsh.text.RenderPrintWriter
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
import rx.Observable
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertFailsWith
|
|
||||||
|
|
||||||
class InteractiveShellTest {
|
|
||||||
lateinit var inputObjectMapper: ObjectMapper
|
|
||||||
lateinit var cordaRpcOps: CordaRPCOps
|
|
||||||
lateinit var invocationContext: InvocationContext<Map<Any, Any>>
|
|
||||||
lateinit var printWriter: RenderPrintWriter
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
inputObjectMapper = objectMapperWithClassLoader(InteractiveShell.getCordappsClassloader())
|
|
||||||
cordaRpcOps = mock()
|
|
||||||
invocationContext = mock()
|
|
||||||
printWriter = mock()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
|
||||||
|
|
||||||
private val ALICE = getTestPartyAndCertificate(ALICE_NAME, generateKeyPair().public)
|
|
||||||
private val BOB = getTestPartyAndCertificate(BOB_NAME, generateKeyPair().public)
|
|
||||||
private val ALICE_NODE_INFO = NodeInfo(listOf(NetworkHostAndPort("localhost", 8080)), listOf(ALICE), 1, 1)
|
|
||||||
private val BOB_NODE_INFO = NodeInfo(listOf(NetworkHostAndPort("localhost", 80)), listOf(BOB), 1, 1)
|
|
||||||
private val NODE_INFO_JSON_PAYLOAD =
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"addresses" : [ "localhost:8080" ],
|
|
||||||
"legalIdentitiesAndCerts" : [ "O=Alice Corp, L=Madrid, C=ES" ],
|
|
||||||
"platformVersion" : 1,
|
|
||||||
"serial" : 1
|
|
||||||
}
|
|
||||||
""".trimIndent()
|
|
||||||
private val NODE_INFO_YAML_PAYLOAD =
|
|
||||||
"""
|
|
||||||
addresses:
|
|
||||||
- "localhost:8080"
|
|
||||||
legalIdentitiesAndCerts:
|
|
||||||
- "O=Alice Corp, L=Madrid, C=ES"
|
|
||||||
platformVersion: 1
|
|
||||||
serial: 1
|
|
||||||
|
|
||||||
""".trimIndent()
|
|
||||||
private val NETWORK_MAP_JSON_PAYLOAD =
|
|
||||||
"""
|
|
||||||
[ {
|
|
||||||
"addresses" : [ "localhost:8080" ],
|
|
||||||
"legalIdentitiesAndCerts" : [ "O=Alice Corp, L=Madrid, C=ES" ],
|
|
||||||
"platformVersion" : 1,
|
|
||||||
"serial" : 1
|
|
||||||
}, {
|
|
||||||
"addresses" : [ "localhost:80" ],
|
|
||||||
"legalIdentitiesAndCerts" : [ "O=Bob Plc, L=Rome, C=IT" ],
|
|
||||||
"platformVersion" : 1,
|
|
||||||
"serial" : 1
|
|
||||||
} ]
|
|
||||||
""".trimIndent()
|
|
||||||
private val NETWORK_MAP_YAML_PAYLOAD =
|
|
||||||
"""
|
|
||||||
- addresses:
|
|
||||||
- "localhost:8080"
|
|
||||||
legalIdentitiesAndCerts:
|
|
||||||
- "O=Alice Corp, L=Madrid, C=ES"
|
|
||||||
platformVersion: 1
|
|
||||||
serial: 1
|
|
||||||
- addresses:
|
|
||||||
- "localhost:80"
|
|
||||||
legalIdentitiesAndCerts:
|
|
||||||
- "O=Bob Plc, L=Rome, C=IT"
|
|
||||||
platformVersion: 1
|
|
||||||
serial: 1
|
|
||||||
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private val ids = InMemoryIdentityService(listOf(megaCorp.identity), DEV_ROOT_CA.certificate)
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private val om = JacksonSupport.createInMemoryMapper(ids, YAMLFactory())
|
|
||||||
|
|
||||||
private fun check(input: String, expected: String) {
|
|
||||||
var output: String? = null
|
|
||||||
InteractiveShell.runFlowFromString({ clazz, args ->
|
|
||||||
val instance = clazz.getConstructor(*args.map { it!!::class.java }.toTypedArray()).newInstance(*args) as FlowA
|
|
||||||
output = instance.a
|
|
||||||
val future = openFuture<String>()
|
|
||||||
future.set("ABC")
|
|
||||||
FlowProgressHandleImpl(StateMachineRunId.createRandom(), future, Observable.just("Some string"))
|
|
||||||
}, input, FlowA::class.java, om)
|
|
||||||
assertEquals(expected, output!!, input)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun objectMapperWithClassLoader(classLoader: ClassLoader?): ObjectMapper {
|
|
||||||
val objectMapper = JacksonSupport.createNonRpcMapper()
|
|
||||||
val tf = TypeFactory.defaultInstance().withClassLoader(classLoader)
|
|
||||||
objectMapper.typeFactory = tf
|
|
||||||
|
|
||||||
return objectMapper
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun flowStartSimple() {
|
|
||||||
check("a: Hi there", "Hi there")
|
|
||||||
check("b: 12", "12")
|
|
||||||
check("b: 12, c: Yo", "12Yo")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun flowStartWithComplexTypes() = check("amount: £10", "10.00 GBP")
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun flowStartWithNestedTypes() = check(
|
|
||||||
input = "pair: { first: $100.12, second: df489807f81c8c8829e509e1bcb92e6692b9dd9d624b7456435cb2f51dc82587 }",
|
|
||||||
expected = "(100.12 USD, DF489807F81C8C8829E509E1BCB92E6692B9DD9D624B7456435CB2F51DC82587)"
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun flowStartWithArrayType() = check(
|
|
||||||
input = "c: [ One, Two, Three, Four ]",
|
|
||||||
expected = "One+Two+Three+Four"
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun flowStartWithUserAmount() = check(
|
|
||||||
input = """b: 500, amount: { "quantity": 10001, "token":{ "label": "of value" } }""",
|
|
||||||
expected = "10501 of value"
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun flowStartWithArrayOfNestedTypes() = check(
|
|
||||||
input = """amounts: [ { "quantity": 10, "token": { "label": "(1)" } }, { "quantity": 200, "token": { "label": "(2)" } } ]""",
|
|
||||||
expected = "10 (1)++200 (2)"
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test(expected = InteractiveShell.NoApplicableConstructor::class, timeout=300_000)
|
|
||||||
fun flowStartNoArgs() = check("", "")
|
|
||||||
|
|
||||||
@Test(expected = InteractiveShell.NoApplicableConstructor::class, timeout=300_000)
|
|
||||||
fun flowMissingParam() = check("d: Yo", "")
|
|
||||||
|
|
||||||
@Test(expected = InteractiveShell.NoApplicableConstructor::class, timeout=300_000)
|
|
||||||
fun flowTooManyParams() = check("b: 12, c: Yo, d: Bar", "")
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun niceTypeNamesInErrors() {
|
|
||||||
val e = assertFailsWith<InteractiveShell.NoApplicableConstructor> {
|
|
||||||
check("", expected = "")
|
|
||||||
}
|
|
||||||
val correct = setOf(
|
|
||||||
"[amounts: Amount<UserValue>[]]: missing parameter amounts",
|
|
||||||
"[amount: Amount<Currency>]: missing parameter amount",
|
|
||||||
"[pair: Pair<Amount<Currency>, SecureHash.SHA256>]: missing parameter pair",
|
|
||||||
"[party: Party]: missing parameter party",
|
|
||||||
"[b: Integer, amount: Amount<UserValue>]: missing parameter b",
|
|
||||||
"[c: String[]]: missing parameter c",
|
|
||||||
"[b: Integer, c: String]: missing parameter b",
|
|
||||||
"[a: String]: missing parameter a",
|
|
||||||
"[b: Integer]: missing parameter b"
|
|
||||||
)
|
|
||||||
val errors = e.errors.toHashSet()
|
|
||||||
errors.removeAll(correct)
|
|
||||||
assert(errors.isEmpty()) { errors.joinToString(", ") }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun party() = check("party: \"${megaCorp.name}\"", megaCorp.name.toString())
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun runRpcFromStringWithCustomTypeResult() {
|
|
||||||
val command = listOf("nodeInfo")
|
|
||||||
whenever(cordaRpcOps.nodeInfo()).thenReturn(ALICE_NODE_INFO)
|
|
||||||
|
|
||||||
InteractiveShell.setOutputFormat(InteractiveShell.OutputFormat.YAML)
|
|
||||||
InteractiveShell.runRPCFromString(command, printWriter, invocationContext, cordaRpcOps, inputObjectMapper)
|
|
||||||
verify(printWriter).println(NODE_INFO_YAML_PAYLOAD)
|
|
||||||
|
|
||||||
|
|
||||||
InteractiveShell.setOutputFormat(InteractiveShell.OutputFormat.JSON)
|
|
||||||
InteractiveShell.runRPCFromString(command, printWriter, invocationContext, cordaRpcOps, inputObjectMapper)
|
|
||||||
verify(printWriter).println(NODE_INFO_JSON_PAYLOAD.replace("\n", System.lineSeparator()))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun runRpcFromStringWithCollectionsResult() {
|
|
||||||
val command = listOf("networkMapSnapshot")
|
|
||||||
whenever(cordaRpcOps.networkMapSnapshot()).thenReturn(listOf(ALICE_NODE_INFO, BOB_NODE_INFO))
|
|
||||||
|
|
||||||
InteractiveShell.setOutputFormat(InteractiveShell.OutputFormat.YAML)
|
|
||||||
InteractiveShell.runRPCFromString(command, printWriter, invocationContext, cordaRpcOps, inputObjectMapper)
|
|
||||||
verify(printWriter).println(NETWORK_MAP_YAML_PAYLOAD)
|
|
||||||
|
|
||||||
InteractiveShell.setOutputFormat(InteractiveShell.OutputFormat.JSON)
|
|
||||||
InteractiveShell.runRPCFromString(command, printWriter, invocationContext, cordaRpcOps, inputObjectMapper)
|
|
||||||
verify(printWriter).println(NETWORK_MAP_JSON_PAYLOAD.replace("\n", System.lineSeparator()))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun killFlowWithNonsenseID() {
|
|
||||||
InteractiveShell.killFlowById("nonsense", printWriter, cordaRpcOps, om)
|
|
||||||
verify(printWriter).println("Cannot parse flow ID of 'nonsense' - expecting a UUID.", Decoration.bold, Color.red)
|
|
||||||
verify(printWriter).flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun killFlowFailure() {
|
|
||||||
val runId = StateMachineRunId.createRandom()
|
|
||||||
whenever(cordaRpcOps.killFlow(any())).thenReturn(false)
|
|
||||||
|
|
||||||
InteractiveShell.killFlowById(runId.uuid.toString(), printWriter, cordaRpcOps, om)
|
|
||||||
verify(cordaRpcOps).killFlow(runId)
|
|
||||||
verify(printWriter).println("Failed to kill flow $runId", Decoration.bold, Color.red)
|
|
||||||
verify(printWriter).flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun killFlowSuccess() {
|
|
||||||
val runId = StateMachineRunId.createRandom()
|
|
||||||
whenever(cordaRpcOps.killFlow(any())).thenReturn(true)
|
|
||||||
|
|
||||||
InteractiveShell.killFlowById(runId.uuid.toString(), printWriter, cordaRpcOps, om)
|
|
||||||
verify(cordaRpcOps).killFlow(runId)
|
|
||||||
verify(printWriter).println("Killed flow $runId", Decoration.bold, Color.yellow)
|
|
||||||
verify(printWriter).flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ToStringSerialize
|
|
||||||
data class UserValue(@JsonProperty("label") val label: String) {
|
|
||||||
override fun toString() = label
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNUSED")
|
|
||||||
class FlowA(val a: String) : FlowLogic<String>() {
|
|
||||||
constructor(b: Int?) : this(b.toString())
|
|
||||||
constructor(b: Int?, c: String) : this(b.toString() + c)
|
|
||||||
constructor(amount: Amount<Currency>) : this(amount.toString())
|
|
||||||
constructor(pair: Pair<Amount<Currency>, SecureHash.SHA256>) : this(pair.toString())
|
|
||||||
constructor(party: Party) : this(party.name.toString())
|
|
||||||
constructor(b: Int?, amount: Amount<UserValue>) : this("${(b ?: 0) + amount.quantity} ${amount.token}")
|
|
||||||
constructor(c: Array<String>) : this(c.joinToString("+"))
|
|
||||||
constructor(amounts: Array<Amount<UserValue>>) : this(amounts.joinToString("++", transform = Amount<UserValue>::toString))
|
|
||||||
|
|
||||||
override val progressTracker = ProgressTracker()
|
|
||||||
override fun call() = a
|
|
||||||
}
|
|
@ -1,122 +0,0 @@
|
|||||||
package net.corda.tools.shell.utilities
|
|
||||||
|
|
||||||
import com.nhaarman.mockito_kotlin.*
|
|
||||||
import net.corda.core.flows.StateMachineRunId
|
|
||||||
import net.corda.core.internal.concurrent.openFuture
|
|
||||||
import net.corda.core.messaging.DataFeed
|
|
||||||
import net.corda.core.messaging.FlowProgressHandleImpl
|
|
||||||
import net.corda.tools.shell.utlities.ANSIProgressRenderer
|
|
||||||
import net.corda.tools.shell.utlities.CRaSHANSIProgressRenderer
|
|
||||||
import org.apache.commons.lang3.SystemUtils
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
|
||||||
import org.crsh.text.RenderPrintWriter
|
|
||||||
import org.fusesource.jansi.Ansi
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
import rx.Observable
|
|
||||||
import rx.subjects.PublishSubject
|
|
||||||
|
|
||||||
class ANSIProgressRendererTest {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val INTENSITY_BOLD_ON_ASCII = "[1m"
|
|
||||||
private const val INTENSITY_OFF_ASCII = "[22m"
|
|
||||||
private const val INTENSITY_FAINT_ON_ASCII = "[2m"
|
|
||||||
|
|
||||||
private const val STEP_1_LABEL = "Running step 1"
|
|
||||||
private const val STEP_2_LABEL = "Running step 2"
|
|
||||||
private const val STEP_3_LABEL = "Running step 3"
|
|
||||||
private const val STEP_4_LABEL = "Running step 4"
|
|
||||||
private const val STEP_5_LABEL = "Running step 5"
|
|
||||||
|
|
||||||
fun stepSuccess(stepLabel: String): String {
|
|
||||||
return if (SystemUtils.IS_OS_WINDOWS) """DONE: $stepLabel""" else """✓ $stepLabel"""
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stepSkipped(stepLabel: String): String {
|
|
||||||
return """ $INTENSITY_FAINT_ON_ASCII$stepLabel$INTENSITY_OFF_ASCII"""
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stepActive(stepLabel: String): String {
|
|
||||||
return if (SystemUtils.IS_OS_WINDOWS)
|
|
||||||
"""CURRENT: $INTENSITY_BOLD_ON_ASCII$stepLabel$INTENSITY_OFF_ASCII"""
|
|
||||||
else
|
|
||||||
"""▶︎ $INTENSITY_BOLD_ON_ASCII$stepLabel$INTENSITY_OFF_ASCII"""
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stepNotRun(stepLabel: String): String {
|
|
||||||
return """ $stepLabel"""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lateinit var printWriter: RenderPrintWriter
|
|
||||||
lateinit var progressRenderer: ANSIProgressRenderer
|
|
||||||
lateinit var indexSubject: PublishSubject<Int>
|
|
||||||
lateinit var feedSubject: PublishSubject<List<Pair<Int, String>>>
|
|
||||||
lateinit var flowProgressHandle: FlowProgressHandleImpl<*>
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
printWriter = mock()
|
|
||||||
progressRenderer = CRaSHANSIProgressRenderer(printWriter)
|
|
||||||
indexSubject = PublishSubject.create<Int>()
|
|
||||||
feedSubject = PublishSubject.create<List<Pair<Int, String>>>()
|
|
||||||
val stepsTreeIndexFeed = DataFeed<Int, Int>(0, indexSubject)
|
|
||||||
val stepsTreeFeed = DataFeed<List<Pair<Int, String>>, List<Pair<Int, String>>>(listOf(), feedSubject)
|
|
||||||
flowProgressHandle = FlowProgressHandleImpl(StateMachineRunId.createRandom(), openFuture<String>(), Observable.empty(), stepsTreeIndexFeed, stepsTreeFeed)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkTrackingState(captor: KArgumentCaptor<Ansi>, updates: Int, trackerState: List<String>) {
|
|
||||||
verify(printWriter, times(updates)).print(captor.capture())
|
|
||||||
assertThat(captor.lastValue.toString()).containsSubsequence(trackerState)
|
|
||||||
verify(printWriter, times(updates)).flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `test that steps are rendered appropriately depending on their status`() {
|
|
||||||
progressRenderer.render(flowProgressHandle)
|
|
||||||
feedSubject.onNext(listOf(Pair(0, STEP_1_LABEL), Pair(0, STEP_2_LABEL), Pair(0, STEP_3_LABEL)))
|
|
||||||
// The flow is currently at step 3, while step 1 has been completed and step 2 has been skipped.
|
|
||||||
indexSubject.onNext(0)
|
|
||||||
indexSubject.onNext(2)
|
|
||||||
|
|
||||||
val captor = argumentCaptor<Ansi>()
|
|
||||||
checkTrackingState(captor, 2, listOf(stepSuccess(STEP_1_LABEL), stepSkipped(STEP_2_LABEL), stepActive(STEP_3_LABEL)))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `changing tree causes correct steps to be marked as done`() {
|
|
||||||
progressRenderer.render(flowProgressHandle)
|
|
||||||
feedSubject.onNext(listOf(Pair(0, STEP_1_LABEL), Pair(1, STEP_2_LABEL), Pair(1, STEP_3_LABEL), Pair(0, STEP_4_LABEL), Pair(0, STEP_5_LABEL)))
|
|
||||||
indexSubject.onNext(0)
|
|
||||||
indexSubject.onNext(1)
|
|
||||||
indexSubject.onNext(2)
|
|
||||||
|
|
||||||
val captor = argumentCaptor<Ansi>()
|
|
||||||
checkTrackingState(captor, 3, listOf(stepSuccess(STEP_1_LABEL), stepSuccess(STEP_2_LABEL), stepActive(STEP_3_LABEL)))
|
|
||||||
|
|
||||||
feedSubject.onNext(listOf(Pair(0, STEP_1_LABEL), Pair(0, STEP_4_LABEL), Pair(0, STEP_5_LABEL)))
|
|
||||||
checkTrackingState(captor, 4, listOf(stepActive(STEP_1_LABEL), stepNotRun(STEP_4_LABEL), stepNotRun(STEP_5_LABEL)))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(timeout=300_000)
|
|
||||||
fun `duplicate steps in different children handled correctly`() {
|
|
||||||
val captor = argumentCaptor<Ansi>()
|
|
||||||
progressRenderer.render(flowProgressHandle)
|
|
||||||
feedSubject.onNext(listOf(Pair(0, STEP_1_LABEL), Pair(0, STEP_2_LABEL)))
|
|
||||||
indexSubject.onNext(0)
|
|
||||||
|
|
||||||
checkTrackingState(captor, 1, listOf(stepActive(STEP_1_LABEL), stepNotRun(STEP_2_LABEL)))
|
|
||||||
|
|
||||||
feedSubject.onNext(listOf(Pair(0, STEP_1_LABEL), Pair(1, STEP_3_LABEL), Pair(0, STEP_2_LABEL), Pair(1, STEP_3_LABEL)))
|
|
||||||
indexSubject.onNext(1)
|
|
||||||
indexSubject.onNext(2)
|
|
||||||
indexSubject.onNext(3)
|
|
||||||
|
|
||||||
checkTrackingState(captor, 5, listOf(stepSuccess(STEP_1_LABEL), stepSuccess(STEP_3_LABEL), stepSuccess(STEP_2_LABEL), stepActive(STEP_3_LABEL)))
|
|
||||||
|
|
||||||
feedSubject.onNext(listOf(Pair(0, STEP_1_LABEL), Pair(1, STEP_3_LABEL), Pair(0, STEP_2_LABEL), Pair(1, STEP_3_LABEL), Pair(2, STEP_4_LABEL)))
|
|
||||||
|
|
||||||
checkTrackingState(captor, 6, listOf(stepSuccess(STEP_1_LABEL), stepSuccess(STEP_3_LABEL), stepSuccess(STEP_2_LABEL), stepActive(STEP_3_LABEL), stepNotRun(STEP_4_LABEL)))
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
node {
|
|
||||||
addresses {
|
|
||||||
rpc {
|
|
||||||
host : "alocalhost"
|
|
||||||
port : 1234
|
|
||||||
}
|
|
||||||
}
|
|
||||||
user : demo
|
|
||||||
password : abcd1234
|
|
||||||
}
|
|
||||||
extensions {
|
|
||||||
cordapps {
|
|
||||||
path : "/x/y/cordapps"
|
|
||||||
}
|
|
||||||
sshd {
|
|
||||||
enabled : "true"
|
|
||||||
port : 2223
|
|
||||||
}
|
|
||||||
commands {
|
|
||||||
path : /x/y/commands
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ssl {
|
|
||||||
truststore {
|
|
||||||
path : "/x/y/truststore.jks"
|
|
||||||
type : "JKS"
|
|
||||||
password : "pass2"
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user