mirror of
https://github.com/corda/corda.git
synced 2025-06-21 08:40:03 +00:00
committed by
Rick Parker
parent
96e6313fbd
commit
e6f9b46584
@ -55,7 +55,7 @@ public class FlowShellCommand extends InteractiveShellCommand {
|
||||
ANSIProgressRenderer ansiProgressRenderer,
|
||||
ObjectMapper om) {
|
||||
if (name == null) {
|
||||
out.println("You must pass a name for the flow, see 'man flow'", Color.red);
|
||||
out.println("You must pass a name for the flow, see 'man flow'", Decoration.bold, Color.red);
|
||||
return;
|
||||
}
|
||||
String inp = input == null ? "" : String.join(" ", input).trim();
|
||||
|
@ -8,6 +8,7 @@ import org.crsh.cli.Command;
|
||||
import org.crsh.cli.Man;
|
||||
import org.crsh.cli.Usage;
|
||||
import org.crsh.text.Color;
|
||||
import org.crsh.text.Decoration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -27,7 +28,7 @@ public class HashLookupShellCommand extends InteractiveShellCommand {
|
||||
logger.info("Executing command \"hash-lookup\".");
|
||||
|
||||
if (txIdHash == null) {
|
||||
out.println("Please provide a hexadecimal transaction Id hash value, see 'man hash-lookup'", Color.red);
|
||||
out.println("Please provide a hexadecimal transaction Id hash value, see 'man hash-lookup'", Decoration.bold, Color.red);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -38,7 +39,7 @@ public class HashLookupShellCommand extends InteractiveShellCommand {
|
||||
try {
|
||||
txIdHashParsed = SecureHash.parse(txIdHash);
|
||||
} catch (IllegalArgumentException e) {
|
||||
out.println("The provided string is not a valid hexadecimal SHA-256 hash value", Color.red);
|
||||
out.println("The provided string is not a valid hexadecimal SHA-256 hash value", Decoration.bold, Color.red);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -53,7 +54,7 @@ public class HashLookupShellCommand extends InteractiveShellCommand {
|
||||
SecureHash found = match.get();
|
||||
out.println("Found a matching transaction with Id: " + found.toString());
|
||||
} else {
|
||||
out.println("No matching transaction found", Color.red);
|
||||
out.println("No matching transaction found", Decoration.bold, Color.red);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ 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
|
||||
@ -50,6 +51,7 @@ 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
|
||||
@ -87,6 +89,8 @@ import kotlin.concurrent.thread
|
||||
// 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)
|
||||
@ -128,14 +132,21 @@ object InteractiveShell {
|
||||
rpcConn = connection
|
||||
connection.proxy as InternalCordaRPCOps
|
||||
}
|
||||
_startShell(configuration, classLoader)
|
||||
launchShell(configuration, standalone, classLoader)
|
||||
}
|
||||
|
||||
private fun _startShell(configuration: ShellConfiguration, classLoader: ClassLoader? = null) {
|
||||
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.
|
||||
@ -183,7 +194,14 @@ object InteractiveShell {
|
||||
"Commands to extract information about checkpoints stored within the node",
|
||||
CheckpointShellCommand::class.java
|
||||
)
|
||||
shell = ShellLifecycle(configuration.commandsDirectory).start(config, configuration.user, configuration.password)
|
||||
|
||||
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 = {}) {
|
||||
@ -210,7 +228,7 @@ object InteractiveShell {
|
||||
}
|
||||
}
|
||||
|
||||
class ShellLifecycle(private val shellCommands: Path) : PluginLifeCycle() {
|
||||
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)
|
||||
@ -243,7 +261,8 @@ object InteractiveShell {
|
||||
this.config = config
|
||||
start(context)
|
||||
ops = makeRPCOps(rpcOps, localUserName, localUserPassword)
|
||||
return context.getPlugin(ShellFactory::class.java).create(null, CordaSSHAuthInfo(false, ops, StdoutANSIProgressRenderer))
|
||||
return context.getPlugin(ShellFactory::class.java).create(null, CordaSSHAuthInfo(false, ops,
|
||||
StdoutANSIProgressRenderer), shellSafety)
|
||||
}
|
||||
}
|
||||
|
||||
@ -317,15 +336,15 @@ object InteractiveShell {
|
||||
val matches = try {
|
||||
rpcOps.registeredFlows().filter { nameFragment in it }
|
||||
} catch (e: PermissionException) {
|
||||
output.println(e.message ?: "Access denied", Color.red)
|
||||
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.", Color.red)
|
||||
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", Color.yellow) }
|
||||
matches.forEachIndexed { i, s -> output.println("${i + 1}. $s", Decoration.bold, Color.yellow) }
|
||||
return
|
||||
}
|
||||
|
||||
@ -364,10 +383,10 @@ object InteractiveShell {
|
||||
}
|
||||
output.println("Flow completed with result: ${stateObservable.returnValue.get()}")
|
||||
} catch (e: NoApplicableConstructor) {
|
||||
output.println("No matching constructor found:", Color.red)
|
||||
e.errors.forEach { output.println("- $it", Color.red) }
|
||||
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", Color.red)
|
||||
output.println(e.message ?: "Access denied", Decoration.bold, Color.red)
|
||||
} catch (e: ExecutionException) {
|
||||
// ignoring it as already logged by the progress handler subscriber
|
||||
} finally {
|
||||
@ -421,7 +440,7 @@ object InteractiveShell {
|
||||
val runId = try {
|
||||
inputObjectMapper.readValue(id, StateMachineRunId::class.java)
|
||||
} catch (e: JsonMappingException) {
|
||||
output.println("Cannot parse flow ID of '$id' - expecting a UUID.", Color.red)
|
||||
output.println("Cannot parse flow ID of '$id' - expecting a UUID.", Decoration.bold, Color.red)
|
||||
log.error("Failed to parse flow ID", e)
|
||||
return
|
||||
}
|
||||
@ -429,14 +448,14 @@ object InteractiveShell {
|
||||
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, Color.red)
|
||||
output.println(msg, Decoration.bold, Color.red)
|
||||
log.warn(msg)
|
||||
return
|
||||
}
|
||||
if (rpcOps.killFlow(runId)) {
|
||||
output.println("Killed flow $runId", Color.yellow)
|
||||
output.println("Killed flow $runId", Decoration.bold, Color.yellow)
|
||||
} else {
|
||||
output.println("Failed to kill flow $runId", Color.red)
|
||||
output.println("Failed to kill flow $runId", Decoration.bold, Color.red)
|
||||
}
|
||||
} finally {
|
||||
output.flush()
|
||||
@ -568,7 +587,7 @@ object InteractiveShell {
|
||||
// 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.", Color.yellow)
|
||||
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)
|
||||
@ -603,12 +622,12 @@ object InteractiveShell {
|
||||
}
|
||||
}
|
||||
} catch (e: StringToMethodCallParser.UnparseableCallException) {
|
||||
out.println(e.message, Color.red)
|
||||
out.println(e.message, Decoration.bold, Color.red)
|
||||
if (e !is StringToMethodCallParser.UnparseableCallException.NoSuchFile) {
|
||||
out.println("Please try 'man run' to learn what syntax is acceptable")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
out.println("RPC failed: ${e.rootCause}", Color.red)
|
||||
out.println("RPC failed: ${e.rootCause}", Decoration.bold, Color.red)
|
||||
} finally {
|
||||
InputStreamSerializer.invokeContext = null
|
||||
InputStreamDeserializer.closeAll()
|
||||
|
@ -9,13 +9,15 @@ data class ShellConfiguration(
|
||||
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 SSH_PORT = 2222
|
||||
const val COMMANDS_DIR = "shell-commands"
|
||||
const val CORDAPPS_DIR = "cordapps"
|
||||
const val SSHD_HOSTKEY_DIR = "ssh"
|
||||
|
@ -31,6 +31,7 @@ import net.corda.testing.core.getTestPartyAndCertificate
|
||||
import net.corda.testing.internal.DEV_ROOT_CA
|
||||
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
|
||||
@ -235,7 +236,7 @@ class InteractiveShellTest {
|
||||
@Test
|
||||
fun killFlowWithNonsenseID() {
|
||||
InteractiveShell.killFlowById("nonsense", printWriter, cordaRpcOps, om)
|
||||
verify(printWriter).println("Cannot parse flow ID of 'nonsense' - expecting a UUID.", Color.red)
|
||||
verify(printWriter).println("Cannot parse flow ID of 'nonsense' - expecting a UUID.", Decoration.bold, Color.red)
|
||||
verify(printWriter).flush()
|
||||
}
|
||||
|
||||
@ -246,7 +247,7 @@ class InteractiveShellTest {
|
||||
|
||||
InteractiveShell.killFlowById(runId.uuid.toString(), printWriter, cordaRpcOps, om)
|
||||
verify(cordaRpcOps).killFlow(runId)
|
||||
verify(printWriter).println("Failed to kill flow $runId", Color.red)
|
||||
verify(printWriter).println("Failed to kill flow $runId", Decoration.bold, Color.red)
|
||||
verify(printWriter).flush()
|
||||
}
|
||||
|
||||
@ -257,7 +258,7 @@ class InteractiveShellTest {
|
||||
|
||||
InteractiveShell.killFlowById(runId.uuid.toString(), printWriter, cordaRpcOps, om)
|
||||
verify(cordaRpcOps).killFlow(runId)
|
||||
verify(printWriter).println("Killed flow $runId", Color.yellow)
|
||||
verify(printWriter).println("Killed flow $runId", Decoration.bold, Color.yellow)
|
||||
verify(printWriter).flush()
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user