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:
Dan Newton 2022-01-12 11:54:18 +00:00 committed by GitHub
parent 78aed771b2
commit 56c9d6404f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 178 additions and 4514 deletions

View File

@ -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')

View File

@ -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')

View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -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")
}
}

View File

@ -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

View File

@ -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(

View File

@ -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()

View File

@ -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) {

View File

@ -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
)

View File

@ -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

View File

@ -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'

View File

@ -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"

View File

@ -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``

View File

@ -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'
}

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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()}>>> "
}

View File

@ -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)
}
}

View File

@ -1,6 +0,0 @@
package net.corda.tools.shell
import net.corda.testing.CliBackwardsCompatibleTest
class StandaloneShellCompatibilityTest : CliBackwardsCompatibleTest(StandaloneShell::class.java)

View File

@ -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"
}
}

View File

@ -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: []

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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()
}
}

View File

@ -1,8 +0,0 @@
user=demo1
baseDirectory="/Users/szymonsztuka/Documents/shell-config"
hostAndPort="localhost:10006"
sshdPort=2223
ssl {
keyStorePassword=password
trustStorePassword=password
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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));
}
}

View File

@ -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");
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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));
}
}

View File

@ -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 ?: "")
)
)
}
}
}
}

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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()
}
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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"
}
}

View File

@ -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()
}
}

View File

@ -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()}>>> "
}

View File

@ -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());
}
}
}

View File

@ -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");
}
}

View File

@ -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)
}
}

View File

@ -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())
}
}
}

View File

@ -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
}

View File

@ -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)))
}
}

View File

@ -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"
}
}