mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +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.commons_collections_version = '4.3'
|
||||
ext.beanutils_version = '1.9.4'
|
||||
ext.crash_version = '1.7.5'
|
||||
ext.jsr305_version = constants.getProperty("jsr305Version")
|
||||
ext.shiro_version = '1.4.1'
|
||||
ext.artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion')
|
||||
|
@ -94,7 +94,7 @@ processTestResources {
|
||||
dependencies {
|
||||
compile project(':node-api')
|
||||
compile project(':client:rpc')
|
||||
compile project(':tools:shell')
|
||||
compile project(':client:jackson')
|
||||
compile project(':tools:cliutils')
|
||||
compile project(':common-validation')
|
||||
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.rpc.proxies.AuthenticatedRpcOpsProxy
|
||||
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.FinalityHandler
|
||||
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.config.NodeConfiguration
|
||||
import net.corda.node.services.config.rpc.NodeRpcOptions
|
||||
import net.corda.node.services.config.shell.determineUnsafeUsers
|
||||
import net.corda.node.services.config.shell.toShellConfig
|
||||
import net.corda.node.services.config.shell.toShellConfigMap
|
||||
import net.corda.node.services.config.shouldInitCrashShell
|
||||
import net.corda.node.services.diagnostics.NodeDiagnosticsService
|
||||
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.contextDatabase
|
||||
import net.corda.nodeapi.internal.persistence.withoutDatabaseAccess
|
||||
import net.corda.tools.shell.InteractiveShell
|
||||
import org.apache.activemq.artemis.utils.ReusableLatch
|
||||
import org.jolokia.jvmagent.JolokiaServer
|
||||
import org.jolokia.jvmagent.JolokiaServerConfig
|
||||
@ -689,16 +688,11 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
|
||||
open fun startShell() {
|
||||
if (configuration.shouldInitCrashShell()) {
|
||||
val shellConfiguration = configuration.toShellConfig()
|
||||
shellConfiguration.sshdPort?.let {
|
||||
val shellConfiguration = configuration.toShellConfigMap()
|
||||
shellConfiguration["sshdPort"]?.let {
|
||||
log.info("Binding Shell SSHD server on port $it.")
|
||||
}
|
||||
|
||||
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)
|
||||
InteractiveShell.startShellIfInstalled(configuration, shellConfiguration, cordappLoader)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,21 +8,42 @@ import net.corda.cliutils.printError
|
||||
import net.corda.common.logging.CordaVersion
|
||||
import net.corda.common.logging.errorReporting.CordaErrorContextProvider
|
||||
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.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.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.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.contextLogger
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.*
|
||||
import net.corda.common.logging.errorReporting.ErrorReporting
|
||||
import net.corda.common.logging.errorReporting.report
|
||||
import net.corda.node.NodeCmdLineOptions
|
||||
import net.corda.node.SerialFilter
|
||||
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.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.logRawConfig
|
||||
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.persistence.CouldNotCreateDataSourceException
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException
|
||||
import net.corda.tools.shell.InteractiveShell
|
||||
import org.fusesource.jansi.Ansi
|
||||
import org.slf4j.bridge.SLF4JBridgeHandler
|
||||
import picocli.CommandLine.Mixin
|
||||
import java.io.IOException
|
||||
import java.io.RandomAccessFile
|
||||
import java.lang.NullPointerException
|
||||
import java.lang.management.ManagementFactory
|
||||
import java.net.InetAddress
|
||||
import java.nio.channels.UnresolvedAddressException
|
||||
@ -236,29 +255,25 @@ open class NodeStartup : NodeStartupLogging {
|
||||
val loadedCodapps = node.services.cordappProvider.cordapps.filter { it.isLoaded }
|
||||
logLoadedCorDapps(loadedCodapps)
|
||||
|
||||
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
|
||||
val name = nodeInfo.legalIdentitiesAndCerts.first().name.organisation
|
||||
Node.printBasicNodeInfo("Node for \"$name\" started up and registered in $elapsed sec")
|
||||
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
|
||||
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.
|
||||
if (node.configuration.shouldStartLocalShell()) {
|
||||
node.startupComplete.then {
|
||||
try {
|
||||
InteractiveShell.runLocalShell(node::stop)
|
||||
} catch (e: Exception) {
|
||||
logger.error("Shell failed to start", e)
|
||||
}
|
||||
// Don't start the shell if there's no console attached.
|
||||
// Look for shell here??
|
||||
if (node.configuration.shouldStartLocalShell()) {
|
||||
InteractiveShell.runLocalShellIfInstalled(node.configuration.baseDirectory, node::stop)
|
||||
}
|
||||
}
|
||||
if (node.configuration.shouldStartSSHDaemon()) {
|
||||
Node.printBasicNodeInfo("SSH server listening on port", node.configuration.sshd!!.port.toString())
|
||||
}
|
||||
},
|
||||
{ th ->
|
||||
logger.error("Unexpected exception during registration", th)
|
||||
})
|
||||
if (node.configuration.shouldStartSSHDaemon()) {
|
||||
Node.printBasicNodeInfo("SSH server listening on port", node.configuration.sshd!!.port.toString())
|
||||
}
|
||||
},
|
||||
{ th ->
|
||||
logger.error("Unexpected exception during registration", th)
|
||||
})
|
||||
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.node.services.config.rpc.NodeRpcOptions
|
||||
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.MutualSslConfiguration
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.notary.experimental.bftsmart.BFTSmartConfig
|
||||
import net.corda.notary.experimental.raft.RaftConfig
|
||||
import net.corda.tools.shell.SSHDConfiguration
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
import java.util.Properties
|
||||
import java.util.UUID
|
||||
import javax.security.auth.x500.X500Principal
|
||||
|
||||
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.seconds
|
||||
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.internal.DEV_PUB_KEY_HASHES
|
||||
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.User
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.tools.shell.SSHDConfiguration
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
import java.util.Properties
|
||||
import java.util.UUID
|
||||
import javax.security.auth.x500.X500Principal
|
||||
|
||||
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.toUUID
|
||||
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.internal.config.User
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel
|
||||
import net.corda.notary.experimental.bftsmart.BFTSmartConfig
|
||||
import net.corda.notary.experimental.raft.RaftConfig
|
||||
import net.corda.tools.shell.SSHDConfiguration
|
||||
|
||||
internal object UserSpec : Configuration.Specification<User>("User") {
|
||||
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) {
|
||||
companion object {
|
||||
@ -11,7 +11,7 @@ data class SSHDConfiguration(val port: Int) {
|
||||
*/
|
||||
@JvmStatic
|
||||
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 {
|
||||
str.toInt()
|
||||
} catch (ex: NumberFormatException) {
|
@ -3,22 +3,23 @@ package net.corda.node.services.config.shell
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.node.internal.clientSslOptionsCompatibleWith
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.tools.shell.ShellConfiguration
|
||||
import net.corda.tools.shell.ShellConfiguration.Companion.COMMANDS_DIR
|
||||
import net.corda.tools.shell.ShellConfiguration.Companion.CORDAPPS_DIR
|
||||
import net.corda.tools.shell.ShellConfiguration.Companion.SSHD_HOSTKEY_DIR
|
||||
|
||||
private const val COMMANDS_DIR = "shell-commands"
|
||||
private const val CORDAPPS_DIR = "cordapps"
|
||||
private const val SSHD_HOSTKEY_DIR = "ssh"
|
||||
|
||||
//re-packs data to Shell specific classes
|
||||
fun NodeConfiguration.toShellConfig() = ShellConfiguration(
|
||||
commandsDirectory = this.baseDirectory / COMMANDS_DIR,
|
||||
cordappsDirectory = this.baseDirectory.toString() / CORDAPPS_DIR,
|
||||
user = INTERNAL_SHELL_USER,
|
||||
password = internalShellPassword,
|
||||
permissions = internalShellPermissions(!this.localShellUnsafe),
|
||||
localShellAllowExitInSafeMode = this.localShellAllowExitInSafeMode,
|
||||
localShellUnsafe = this.localShellUnsafe,
|
||||
hostAndPort = this.rpcOptions.address,
|
||||
ssl = clientSslOptionsCompatibleWith(this.rpcOptions),
|
||||
sshdPort = this.sshd?.port,
|
||||
sshHostKeyDirectory = this.baseDirectory / SSHD_HOSTKEY_DIR,
|
||||
noLocalShell = this.noLocalShell)
|
||||
fun NodeConfiguration.toShellConfigMap() = mapOf(
|
||||
"commandsDirectory" to this.baseDirectory / COMMANDS_DIR,
|
||||
"cordappsDirectory" to this.baseDirectory.toString() / CORDAPPS_DIR,
|
||||
"user" to INTERNAL_SHELL_USER,
|
||||
"password" to internalShellPassword,
|
||||
"permissions" to internalShellPermissions(!this.localShellUnsafe),
|
||||
"localShellAllowExitInSafeMode" to this.localShellAllowExitInSafeMode,
|
||||
"localShellUnsafe" to this.localShellUnsafe,
|
||||
"hostAndPort" to this.rpcOptions.address,
|
||||
"ssl" to clientSslOptionsCompatibleWith(this.rpcOptions),
|
||||
"sshdPort" to this.sshd?.port,
|
||||
"sshHostKeyDirectory" to this.baseDirectory / SSHD_HOSTKEY_DIR,
|
||||
"noLocalShell" to this.noLocalShell
|
||||
)
|
||||
|
@ -10,10 +10,10 @@ import net.corda.core.internal.div
|
||||
import net.corda.core.internal.toPath
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.node.services.config.shell.SSHDConfiguration
|
||||
import net.corda.nodeapi.internal.config.getBooleanCaseInsensitive
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
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.assertThatThrownBy
|
||||
import org.junit.Assert.assertEquals
|
||||
|
@ -73,8 +73,6 @@ include 'tools:loadtest'
|
||||
include 'tools:graphs'
|
||||
include 'tools:bootstrapper'
|
||||
include 'tools:blobinspector'
|
||||
include 'tools:shell'
|
||||
include 'tools:shell-cli'
|
||||
include 'tools:network-builder'
|
||||
include 'tools:cliutils'
|
||||
include 'tools:worldmap'
|
||||
|
@ -27,6 +27,9 @@ sourceSets {
|
||||
dependencies {
|
||||
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
|
||||
testCompile "org.assertj:assertj-core:$assertj_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