Non-interactive (or non-REPL) command is a command that executes under ignite
top command. Example of non-interactive command:
> ignite node status --node-url http://localhost:10300
The non-REPL fronted is responsible for:
Interactive (or REPL, Read-Eval-Print-Loop) mode can be activated by ignite
command. Example of interactive command:
> ignite [diconnected]> node status --node-url http://localhost:10300
The REPL frontend is responsible for:
After the command is found and all options are parsed, the command class (class annotated with @Command
) builds a CallExecutionPipeline
and runs it.
This is where both REPL and non-REPL frontends meet and start to use common code. Call Execution Pipeline consists of:
CallInput
. The object that will be passed to the Call
. Example UrlCallInput
.Call
. Executable part of the command: call the REST, read the file, check the node status, etc. Example PhysicalTopologyCall
.Decorator
that gets the output from Call
and transforms it to human-readable output. Example TableDecorator
.ExceptionHandlers
. Registry of exception handlers that will handle all exceptions that might be thrown during the call. Example SqlExceptionHandler
.For the interactive mode there is a common situation when a user is not connected to any node and executes some command. The user might forget to connect to the node and always type --node-url
option instead of connecting to the node once and type only commands. So, it is useful to ask the user if he/she wants to connect to the node with the last --node-url
value.
It might be implemented as several checks and read-line operations in every interactive command. To avoid code duplication the Flow
was introduced. Flow
is a DSL for building user interactions such as dialogs, questions, etc. Flow
can be easily integrated with Call
. A simplified example of Flow
that asks a user to connect to the last connected node on the CLI start:
String clusterUrl = stateConfigProvider.get().getProperty(ConfigConstants.LAST_CONNECTED_URL); QuestionUiComponent question = QuestionUiComponent.fromQuestion( "Do you want to connect to the last connected node %s? %s ",UiElements.url(lastConnectedUrl),UiElements.yesNo() ); Flows.acceptQuestion(question, ()->new ConnectCallInput(clusterUrl)) .then(Flows.fromCall(connectCall)) .print() .start(Flowable.empty());
An example of interactive and non-interactive commands that use the common backend:
ignite node config show
@Command(name = "show", description = "Shows node configuration") public class NodeConfigShowCommand extends BaseCommand implements Callable<Integer> { /** Node URL option. */ @Mixin private NodeUrlProfileMixin nodeUrl; /** Configuration selector option. */ @Parameters(arity = "0..1", description = "Configuration path selector") private String selector; @Inject private NodeConfigShowCall call; /** {@inheritDoc} */ @Override public Integer call() { return CallExecutionPipeline.builder(call) .inputProvider(this::buildCallInput) .output(spec.commandLine().getOut()) .errOutput(spec.commandLine().getErr()) .decorator(new JsonDecorator()) .build() .runPipeline(); } private NodeConfigShowCallInput buildCallInput() { return NodeConfigShowCallInput.builder() .nodeUrl(nodeUrl.getNodeUrl()) .selector(selector) .build(); } }
node config show
@Command(name = "show", description = "Shows node configuration") public class NodeConfigShowReplCommand extends BaseCommand implements Runnable { /** Node URL option. */ @Mixin private NodeUrlMixin nodeUrl; /** Configuration selector option. */ @Parameters(arity = "0..1", description = "Configuration path selector") private String selector; @Inject private NodeConfigShowCall call; @Inject private ConnectToClusterQuestion question; /** {@inheritDoc} */ @Override public void run() { question.askQuestionIfNotConnected(nodeUrl.getNodeUrl()) .map(this::nodeConfigShowCallInput) .then(Flows.fromCall(call)) .print() .start(); } private NodeConfigShowCallInput nodeConfigShowCallInput(String nodeUrl) { return NodeConfigShowCallInput.builder().selector(selector).nodeUrl(nodeUrl).build(); } }
As you can see, both classes use the same NodeConfigShowCall call
that performs the logic of fetching the node configuration.
Completions in non-interactive mode are generated as shell scripts that have to be sourced into the user’s terminal. See generateAutocompletionScript
in build.gradle
.
But interactive mode is different. There is runtime information that can be used to provide more advanced completions (DynamicCompleter
). For example, if a user is connected to the cluster and wants to see a part of cluster configuration using --selector
option. The configuration key might be fetched from the cluster and passed as a completion candidate. See HoconDynamicCompleter
.
./gradlew clean cliDistZip -x test cd packaging/build/distributions unzip ignite3-cli-3.0.0-SNAPSHOT.zip cd ignite3-cli-3.0.0-SNAPSHOT/ ./bin/ignite3-cli
You can take as an example ignite node status
implementation – NodeStatusCommand
. As a REST call implementation – NodeStatusCall
. To enable the command in CLI, put it as a subcommand to TopLevelCliCommand
.
ItClusterStatusCommandInitializedTest
is an example of integration test for command class.
You can take as an example node status
implementation – NodeStatusReplCommand
. As a REST call implementation – NodeStatusCall
. To enable the command in CLI, put it as a subcommand to TopLevelCliReplCommand
.
ItConnectToClusterTest
is an example of integration test for interactive command.