Non-interactive (or non-REPL) command is a command that executes under ignite top command. Example of non-interactive command:
> ignite node status --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 --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 --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 --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.