CORDA-319: Shell: use ExternalResolver to load our commands

This eliminates JIT java compilation and the consequent need for
tools.jar (which doesn't get shipped in DemoBench). It also makes
development more pleasant by avoiding weird IDE integration issues
that came from having java-in-resources.
This commit is contained in:
Mike Hearn
2017-03-29 18:10:34 +02:00
parent 527e571bc3
commit 577b2c2c22
7 changed files with 72 additions and 51 deletions

View File

@ -141,7 +141,9 @@ dependencies {
compile "io.netty:netty-all:$netty_version" compile "io.netty:netty-all:$netty_version"
// CRaSH: An embeddable monitoring and admin shell with support for adding new commands written in Groovy. // CRaSH: An embeddable monitoring and admin shell with support for adding new commands written in Groovy.
compile "com.github.corda:crash:A2" compile("com.github.corda.crash:crash.shell:9d242da2a10e686f33a3aefc69e4768824ad0716") {
exclude group: "org.slf4j", module: "slf4j-jdk14"
}
// OkHTTP: Simple HTTP library. // OkHTTP: Simple HTTP library.
compile "com.squareup.okhttp3:okhttp:$okhttp_version" compile "com.squareup.okhttp3:okhttp:$okhttp_version"

View File

@ -1,4 +1,4 @@
package net.corda.node; package net.corda.node.shell;
// See the comments at the top of run.java // See the comments at the top of run.java
@ -8,7 +8,7 @@ import org.crsh.text.*;
import java.util.*; import java.util.*;
import static net.corda.node.InteractiveShell.*; import static net.corda.node.shell.InteractiveShell.*;
@Man( @Man(
"Allows you to list and start flows. This is the primary way in which you command the node to change the ledger.\n\n" + "Allows you to list and start flows. This is the primary way in which you command the node to change the ledger.\n\n" +
@ -17,7 +17,7 @@ import static net.corda.node.InteractiveShell.*;
"flow constructors (the right one is picked automatically) are then specified using the same syntax as for the run command." "flow constructors (the right one is picked automatically) are then specified using the same syntax as for the run command."
) )
@Usage("Start a (work)flow on the node. This is how you can change the ledger.") @Usage("Start a (work)flow on the node. This is how you can change the ledger.")
public class flow extends InteractiveShellCommand { public class FlowShellCommand extends InteractiveShellCommand {
// Note that the class name is deliberately lower case, because we want the command the user types to be // Note that the class name is deliberately lower case, because we want the command the user types to be
// lower case. CRaSH should ideally lowercase the command names for us, but it doesn't. // lower case. CRaSH should ideally lowercase the command names for us, but it doesn't.

View File

@ -1,24 +1,16 @@
package net.corda.node; package net.corda.node.shell;
import net.corda.core.messaging.*; import net.corda.core.messaging.*;
import net.corda.jackson.*; import net.corda.jackson.*;
import org.crsh.cli.*; import org.crsh.cli.*;
import org.crsh.command.*; import org.crsh.command.*;
import org.crsh.text.*;
import java.lang.reflect.*;
import java.util.*; import java.util.*;
import java.util.concurrent.*;
import static net.corda.node.InteractiveShell.*; // 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.
// This file is actually compiled at runtime with a bundled Java compiler by CRaSH. That's pretty weak: being able public class RunShellCommand extends InteractiveShellCommand {
// to do this is a neat party trick and means people can write new commands in Java then just drop them into
// their node directory, but it makes the first usage of the command slower for no good reason. There is a PR
// in the upstream CRaSH project that adds an ExternalResolver which might be useful. Then we could convert this
// file to Kotlin too.
public class run extends InteractiveShellCommand {
@Command @Command
@Man( @Man(
"Runs a method from the CordaRPCOps interface, which is the same interface exposed to RPC clients.\n\n" + "Runs a method from the CordaRPCOps interface, which is the same interface exposed to RPC clients.\n\n" +

View File

@ -11,6 +11,7 @@ import net.corda.core.node.Version
import net.corda.core.utilities.Emoji import net.corda.core.utilities.Emoji
import net.corda.node.internal.Node import net.corda.node.internal.Node
import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.config.FullNodeConfiguration
import net.corda.node.shell.InteractiveShell
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
import net.corda.node.utilities.registration.NetworkRegistrationHelper import net.corda.node.utilities.registration.NetworkRegistrationHelper
import org.fusesource.jansi.Ansi import org.fusesource.jansi.Ansi
@ -133,7 +134,11 @@ fun main(args: Array<String>) {
// Don't start the shell if there's no console attached. // Don't start the shell if there's no console attached.
val runShell = !cmdlineOptions.noLocalShell && System.console() != null val runShell = !cmdlineOptions.noLocalShell && System.console() != null
node.startupComplete then { node.startupComplete then {
try {
InteractiveShell.startShell(dir, runShell, cmdlineOptions.sshdServer, node) InteractiveShell.startShell(dir, runShell, cmdlineOptions.sshdServer, node)
} catch(e: Throwable) {
log.error("Shell failed to start", e)
}
} }
} failure { } failure {
log.error("Error during network map registration", it) log.error("Error during network map registration", it)

View File

@ -1,4 +1,4 @@
package net.corda.node package net.corda.node.shell
import com.fasterxml.jackson.core.JsonFactory import com.fasterxml.jackson.core.JsonFactory
import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonGenerator
@ -17,6 +17,7 @@ import net.corda.core.utilities.Emoji
import net.corda.jackson.JacksonSupport import net.corda.jackson.JacksonSupport
import net.corda.jackson.StringToMethodCallParser import net.corda.jackson.StringToMethodCallParser
import net.corda.node.internal.Node import net.corda.node.internal.Node
import net.corda.node.printBasicNodeInfo
import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.FlowStateMachineImpl
import net.corda.node.utilities.ANSIProgressRenderer import net.corda.node.utilities.ANSIProgressRenderer
import net.corda.nodeapi.ArtemisMessagingComponent import net.corda.nodeapi.ArtemisMessagingComponent
@ -26,8 +27,14 @@ import org.crsh.command.InvocationContext
import org.crsh.console.jline.JLineProcessor import org.crsh.console.jline.JLineProcessor
import org.crsh.console.jline.TerminalFactory import org.crsh.console.jline.TerminalFactory
import org.crsh.console.jline.console.ConsoleReader 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.ShellFactory
import org.crsh.standalone.Bootstrap import org.crsh.shell.impl.command.ExternalResolver
import org.crsh.text.Color import org.crsh.text.Color
import org.crsh.text.RenderPrintWriter import org.crsh.text.RenderPrintWriter
import org.crsh.util.InterruptHandler import org.crsh.util.InterruptHandler
@ -53,7 +60,6 @@ import kotlin.concurrent.thread
// TODO: Add command history. // TODO: Add command history.
// TODO: Command completion. // TODO: Command completion.
// TODO: Find a way to inject this directly into CRaSH as a command, without needing JIT source compilation.
// TODO: Do something sensible with commands that return a future. // TODO: Do something sensible with commands that return a future.
// TODO: Configure default renderers, send objects down the pipeline, add commands to do json/xml/yaml outputs. // TODO: Configure default renderers, send objects down the pipeline, add commands to do json/xml/yaml outputs.
// TODO: Add a command to view last N lines/tail/control log4j2 loggers. // TODO: Add a command to view last N lines/tail/control log4j2 loggers.
@ -76,25 +82,6 @@ object InteractiveShell {
Logger.getLogger("").level = Level.OFF // TODO: Is this really needed? Logger.getLogger("").level = Level.OFF // TODO: Is this really needed?
val classpathDriver = ClassPathMountFactory(Thread.currentThread().contextClassLoader)
val fileDriver = FileMountFactory(Utils.getCurrentDirectory())
val extraCommandsPath = (dir / "shell-commands").toAbsolutePath().createDirectories()
val commandsFS = FS.Builder()
.register("file", fileDriver)
.mount("file:" + extraCommandsPath)
.register("classpath", classpathDriver)
.mount("classpath:/net/corda/node/shell/")
.mount("classpath:/crash/commands/")
.build()
// TODO: Re-point to our own conf path.
val confFS = FS.Builder()
.register("classpath", classpathDriver)
.mount("classpath:/crash")
.build()
val bootstrap = Bootstrap(Thread.currentThread().contextClassLoader, confFS, commandsFS)
val config = Properties() val config = Properties()
if (runSSH) { if (runSSH) {
// TODO: Finish and enable SSH access. // TODO: Finish and enable SSH access.
@ -117,16 +104,9 @@ object InteractiveShell {
config["crash.auth.simple.password"] = "admin" config["crash.auth.simple.password"] = "admin"
} }
bootstrap.config = config ExternalResolver.INSTANCE.addCommand("run", "Runs a method from the CordaRPCOps interface on the node.", RunShellCommand::class.java)
bootstrap.setAttributes(mapOf( ExternalResolver.INSTANCE.addCommand("flow", "Start a (work)flow on the node. This is how you can change the ledger.", FlowShellCommand::class.java)
"node" to node, val shell = ShellLifecycle(dir).start(config)
"services" to node.services,
"ops" to node.rpcOps,
"mapper" to yamlInputMapper
))
bootstrap.bootstrap()
// TODO: Automatically set up the JDBC sub-command with a connection to the database.
if (runSSH) { if (runSSH) {
// printBasicNodeInfo("SSH server listening on address", node.configuration.sshdAddress.toString()) // printBasicNodeInfo("SSH server listening on address", node.configuration.sshdAddress.toString())
@ -135,7 +115,7 @@ object InteractiveShell {
// Possibly bring up a local shell in the launching terminal window, unless it's disabled. // Possibly bring up a local shell in the launching terminal window, unless it's disabled.
if (!runLocalShell) if (!runLocalShell)
return return
val shell = bootstrap.context.getPlugin(ShellFactory::class.java).create(null) // TODO: Automatically set up the JDBC sub-command with a connection to the database.
val terminal = TerminalFactory.create() val terminal = TerminalFactory.create()
val consoleReader = ConsoleReader("Corda", FileInputStream(FileDescriptor.`in`), System.out, terminal) val consoleReader = ConsoleReader("Corda", FileInputStream(FileDescriptor.`in`), System.out, terminal)
val jlineProcessor = JLineProcessor(terminal.isAnsiSupported, shell, consoleReader, System.out) val jlineProcessor = JLineProcessor(terminal.isAnsiSupported, shell, consoleReader, System.out)
@ -155,6 +135,47 @@ object InteractiveShell {
} }
} }
class ShellLifecycle(val dir: Path) : PluginLifeCycle() {
fun start(config: Properties): Shell {
val classLoader = this.javaClass.classLoader
val classpathDriver = ClassPathMountFactory(classLoader)
val fileDriver = FileMountFactory(Utils.getCurrentDirectory())
val extraCommandsPath = (dir / "shell-commands").toAbsolutePath().createDirectories()
val commandsFS = FS.Builder()
.register("file", fileDriver)
.mount("file:" + extraCommandsPath)
.register("classpath", classpathDriver)
.mount("classpath:/net/corda/node/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 }
}
}
val attributes = mapOf(
"node" to node,
"services" to node.services,
"ops" to node.rpcOps,
"mapper" to yamlInputMapper
)
val context = PluginContext(discovery, attributes, commandsFS, confFS, classLoader)
context.refresh()
this.config = config
start(context)
return context.getPlugin(ShellFactory::class.java).create(null)
}
}
private val yamlInputMapper: ObjectMapper by lazy { private val yamlInputMapper: ObjectMapper by lazy {
// Return a standard Corda Jackson object mapper, configured to use YAML by default and with extra // Return a standard Corda Jackson object mapper, configured to use YAML by default and with extra
// serializers. // serializers.

View File

@ -1,4 +1,4 @@
package net.corda.node package net.corda.node.shell
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps

View File

@ -14,6 +14,7 @@ import net.corda.core.utilities.DUMMY_PUBKEY_1
import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.UntrustworthyData
import net.corda.jackson.JacksonSupport import net.corda.jackson.JacksonSupport
import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.identity.InMemoryIdentityService
import net.corda.node.shell.InteractiveShell
import org.junit.Test import org.junit.Test
import org.slf4j.Logger import org.slf4j.Logger
import java.util.* import java.util.*