diff --git a/gradle/java/javac.gradle b/gradle/java/javac.gradle index 8a7af8b04d70..3e992b1200ec 100644 --- a/gradle/java/javac.gradle +++ b/gradle/java/javac.gradle @@ -60,7 +60,8 @@ allprojects { "-Xlint:text-blocks", "-proc:none", // proc:none was added because of LOG4J2-1925 / JDK-8186647 "-Xlint:removal", - "--should-stop=ifError=FLOW" // error-prone 2.41 + "--should-stop=ifError=FLOW", // error-prone 2.41 + "-Aproject=${project.group}/${project.name}" ] if (propertyOrDefault("javac.failOnWarnings", true).toBoolean()) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a9222bc94c01..4a7684d42713 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -502,6 +502,7 @@ ow2-asm-tree = { module = "org.ow2.asm:asm-tree", version.ref = "ow2-asm" } # @keep transitive dependency for version alignment perfmark-api = { module = "io.perfmark:perfmark-api", version.ref = "perfmark" } picocli = { module = "info.picocli:picocli", version.ref = "picocli" } +picocli-codegen = { module = "info.picocli:picocli-codegen", version.ref = "picocli" } prometheus-metrics-expositionformats = { module = "io.prometheus:prometheus-metrics-exposition-formats", version.ref = "prometheus-metrics" } prometheus-metrics-model = { module = "io.prometheus:prometheus-metrics-model", version.ref = "prometheus-metrics" } prometheus-simpleclient = { module = "io.prometheus:simpleclient", version.ref = "prometheus-simpleclient" } diff --git a/solr/bin/solr b/solr/bin/solr index eaa417ca90d0..387e942ca5aa 100755 --- a/solr/bin/solr +++ b/solr/bin/solr @@ -812,7 +812,11 @@ if [ $# -gt 0 ]; then shift 2 ;; -h|--help) - print_usage "$SCRIPT_CMD" + if [[ "${SOLR_PICOCLI:-}" == "true" ]]; then + run_tool "$SCRIPT_CMD" --help + else + print_usage "$SCRIPT_CMD" + fi exit 0 ;; -y|--no-prompt) diff --git a/solr/bin/solr.cmd b/solr/bin/solr.cmd index e30cc4416fa8..80097d5da307 100755 --- a/solr/bin/solr.cmd +++ b/solr/bin/solr.cmd @@ -414,8 +414,22 @@ IF "%1"=="--all" goto set_stop_all :parse_general_args REM Print usage of command in case help option included -IF "%1"=="--help" goto usage -IF "%1"=="-h" goto usage +IF "%1"=="--help" goto check_picocli_help +IF "%1"=="-h" goto check_picocli_help +goto after_help_check + +:check_picocli_help +IF "%SOLR_PICOCLI%"=="true" goto run_picocli_help +goto usage + +:run_picocli_help +"%JAVA%" %SOLR_SSL_OPTS% %AUTHC_OPTS% %SOLR_ZK_CREDS_AND_ACLS% %SOLR_TOOL_OPTS% -Dsolr.install.dir="%SOLR_TIP%" ^ + -Dlog4j.configurationFile="file:///%DEFAULT_SERVER_DIR%\resources\log4j2-console.xml" ^ + -classpath "%SOLR_TIP%\lib\*;%DEFAULT_SERVER_DIR%\solr-webapp\webapp\WEB-INF\lib\*;%DEFAULT_SERVER_DIR%\lib\ext\*" ^ + org.apache.solr.cli.SolrCLI %SCRIPT_CMD% --help +goto done + +:after_help_check REM other args supported by all special commands IF "%1"=="-p" goto set_port diff --git a/solr/core/build.gradle b/solr/core/build.gradle index 144a7445f9c7..398a0a2a2ca4 100644 --- a/solr/core/build.gradle +++ b/solr/core/build.gradle @@ -99,7 +99,7 @@ dependencies { implementation libs.commonscli.commonscli implementation libs.picocli - permitUnusedDeclared libs.picocli // will be used when CLI is migrated to picocli + annotationProcessor libs.picocli.codegen implementation libs.locationtech.spatial4j diff --git a/solr/core/gradle.lockfile b/solr/core/gradle.lockfile index ec0442ee4662..512297f3c9a9 100644 --- a/solr/core/gradle.lockfile +++ b/solr/core/gradle.lockfile @@ -37,7 +37,8 @@ com.tdunning:t-digest:3.3=compileClasspath,jarValidation,runtimeClasspath,runtim commons-cli:commons-cli:1.10.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath commons-codec:commons-codec:1.19.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.20.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -info.picocli:picocli:4.7.6=compileClasspath,jarValidation,permitUnusedDeclared,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli-codegen:4.7.6=annotationProcessor +info.picocli:picocli:4.7.6=annotationProcessor,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,testRuntimeClasspath diff --git a/solr/core/src/java/org/apache/solr/cli/ApiTool.java b/solr/core/src/java/org/apache/solr/cli/ApiTool.java index 4a9c86fb8485..b5679429d1ac 100644 --- a/solr/core/src/java/org/apache/solr/cli/ApiTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ApiTool.java @@ -117,4 +117,9 @@ public static ModifiableSolrParams getSolrParamsFromUri(URI uri) { } return paramsMap; } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/AssertTool.java b/solr/core/src/java/org/apache/solr/cli/AssertTool.java index 928065f0a798..064545f9fd3d 100644 --- a/solr/core/src/java/org/apache/solr/cli/AssertTool.java +++ b/solr/core/src/java/org/apache/solr/cli/AssertTool.java @@ -426,6 +426,11 @@ private static boolean runningSolrIsCloud(String url, String credentials) throws } } + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } + public static class AssertionFailureException extends Exception { public AssertionFailureException(String message) { super(message); diff --git a/solr/core/src/java/org/apache/solr/cli/AuthTool.java b/solr/core/src/java/org/apache/solr/cli/AuthTool.java index 5c19ac300d7f..606086529180 100644 --- a/solr/core/src/java/org/apache/solr/cli/AuthTool.java +++ b/solr/core/src/java/org/apache/solr/cli/AuthTool.java @@ -452,4 +452,9 @@ public void runImpl(CommandLine cli) throws Exception { throw new IllegalStateException("Only type=basicAuth supported at the moment."); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/CLIUtils.java b/solr/core/src/java/org/apache/solr/cli/CLIUtils.java index 5653bffa8970..48531f0ac16b 100644 --- a/solr/core/src/java/org/apache/solr/cli/CLIUtils.java +++ b/solr/core/src/java/org/apache/solr/cli/CLIUtils.java @@ -183,6 +183,7 @@ public static String normalizeSolrUrl(String solrUrl, boolean logUrlFormatWarnin * Get the base URL of a live Solr instance from either the --solr-url command-line option or from * ZooKeeper. */ + @Deprecated public static String normalizeSolrUrl(CommandLine cli) throws Exception { String solrUrl = cli.getOptionValue(CommonCLIOptions.SOLR_URL_OPTION); diff --git a/solr/core/src/java/org/apache/solr/cli/CliDefaultValueProvider.java b/solr/core/src/java/org/apache/solr/cli/CliDefaultValueProvider.java new file mode 100644 index 000000000000..99cb2ab95564 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/cli/CliDefaultValueProvider.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.cli; + +import static org.apache.solr.cli.CLIUtils.getCloudSolrClient; +import static org.apache.solr.cli.CLIUtils.normalizeSolrUrl; + +import java.util.Set; +import org.apache.solr.client.solrj.impl.CloudSolrClient; +import org.apache.solr.common.cloud.ZkStateReader; +import org.apache.solr.common.util.EnvUtils; +import picocli.CommandLine; + +/** Provides default values for CLI arguments. */ +public class CliDefaultValueProvider implements CommandLine.IDefaultValueProvider { + @Override + public String defaultValue(CommandLine.Model.ArgSpec argSpec) throws Exception { + return switch (argSpec.paramLabel()) { + case "" -> EnvUtils.getProperty("zkHost"); + case "" -> { + String val = EnvUtils.getProperty("solr.url"); + yield val != null ? val : resolveSolrUrlViaZkHost(argSpec); + } + case "" -> EnvUtils.getProperty("solr.port", "8983"); + case "" -> EnvUtils.getProperty("solr.max.wait.seconds", "0"); + default -> null; + }; + } + + /** + * If no solrUrl is provided on the command line, and SOLR_URL is not set, this method will be + * used to determine the solrUrl from the zkHost. + * + * @param argSpec the argSpec for the solrUrl option + * @return the solrUrl + * @throws Exception if an error occurs + */ + public static String resolveSolrUrlViaZkHost(picocli.CommandLine.Model.ArgSpec argSpec) + throws Exception { + // Find value of zkHost from command line options. The argSpec passed in will be for the + // solrUrl option. + CommandLine.Model.OptionSpec zkHostOption = argSpec.command().findOption("--zk-host"); + + String zkHost = zkHostOption != null ? zkHostOption.getValue() : null; + if (zkHost == null) { + return null; + } + + String solrUrl; + try (CloudSolrClient cloudSolrClient = getCloudSolrClient(zkHost)) { + cloudSolrClient.connect(); + Set liveNodes = cloudSolrClient.getClusterState().getLiveNodes(); + if (liveNodes.isEmpty()) + throw new IllegalStateException( + "No live nodes found! Cannot determine 'solrUrl' from ZooKeeper: " + zkHost); + + String firstLiveNode = liveNodes.iterator().next(); + solrUrl = ZkStateReader.from(cloudSolrClient).getBaseUrlForNodeName(firstLiveNode); + solrUrl = normalizeSolrUrl(solrUrl, false); + } + solrUrl = normalizeSolrUrl(solrUrl); + return solrUrl; + } +} diff --git a/solr/core/src/java/org/apache/solr/cli/ClusterTool.java b/solr/core/src/java/org/apache/solr/cli/ClusterTool.java index 54626e2b7db7..aa70d0582fef 100644 --- a/solr/core/src/java/org/apache/solr/cli/ClusterTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ClusterTool.java @@ -97,4 +97,9 @@ public void runImpl(CommandLine cli) throws Exception { } } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java b/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java index 5623794626cb..a20c5bff853f 100644 --- a/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java @@ -101,4 +101,9 @@ public void runImpl(CommandLine cli) throws Exception { throw (e); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java b/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java index fdd2380f3a19..ba3f188b0b9a 100644 --- a/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java @@ -107,4 +107,9 @@ public void runImpl(CommandLine cli) throws Exception { throw (e); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/ConfigTool.java b/solr/core/src/java/org/apache/solr/cli/ConfigTool.java index a4145168165b..b2a340ba7ed2 100644 --- a/solr/core/src/java/org/apache/solr/cli/ConfigTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ConfigTool.java @@ -137,4 +137,9 @@ public void runImpl(CommandLine cli) throws Exception { } } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/CreateTool.java b/solr/core/src/java/org/apache/solr/cli/CreateTool.java index 74778e7e4904..1f81609f0b78 100644 --- a/solr/core/src/java/org/apache/solr/cli/CreateTool.java +++ b/solr/core/src/java/org/apache/solr/cli/CreateTool.java @@ -352,4 +352,9 @@ private void printDefaultConfigsetWarningIfNecessary(CommandLine cli) { echo(" " + configCommand); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/DeleteTool.java b/solr/core/src/java/org/apache/solr/cli/DeleteTool.java index 77db2184a4cb..07f92894f311 100644 --- a/solr/core/src/java/org/apache/solr/cli/DeleteTool.java +++ b/solr/core/src/java/org/apache/solr/cli/DeleteTool.java @@ -216,4 +216,9 @@ protected void deleteCore(CommandLine cli, SolrClient solrClient) throws Excepti throw new Exception("Failed to delete core '" + coreName + "' due to: " + sse.getMessage()); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/ExportTool.java b/solr/core/src/java/org/apache/solr/cli/ExportTool.java index 6fd43b52a1c8..e95af8640baf 100644 --- a/solr/core/src/java/org/apache/solr/cli/ExportTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ExportTool.java @@ -707,4 +707,9 @@ static long getDocCount(String coreName, SolrClient client, String query) SolrDocumentList sdl = (SolrDocumentList) res.get("response"); return sdl.getNumFound(); } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java b/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java index 670bb0eb6563..2eea27280936 100644 --- a/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java +++ b/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java @@ -213,6 +213,11 @@ protected void runCloudTool(CloudSolrClient cloudSolrClient, CommandLine cli) th new JSONWriter(arr, 2).write(report); echo(arr.toString()); } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } class ReplicaHealth implements Comparable { diff --git a/solr/core/src/java/org/apache/solr/cli/PackageTool.java b/solr/core/src/java/org/apache/solr/cli/PackageTool.java index aaa3649d1be8..9dea8aaba9e9 100644 --- a/solr/core/src/java/org/apache/solr/cli/PackageTool.java +++ b/solr/core/src/java/org/apache/solr/cli/PackageTool.java @@ -378,4 +378,9 @@ public Options getOptions() { .addOption(CommonCLIOptions.CREDENTIALS_OPTION) .addOptionGroup(getConnectionOptions()); } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/PostLogsTool.java b/solr/core/src/java/org/apache/solr/cli/PostLogsTool.java index 82dccf537427..64a99784dacb 100644 --- a/solr/core/src/java/org/apache/solr/cli/PostLogsTool.java +++ b/solr/core/src/java/org/apache/solr/cli/PostLogsTool.java @@ -626,4 +626,9 @@ public static String[] getRequestPurposeNames(Integer reqPurpose) { map.put(ShardRequest.PURPOSE_GET_TERM_STATS, "GET_TERM_STATS"); purposes = Collections.unmodifiableMap(map); } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/PostTool.java b/solr/core/src/java/org/apache/solr/cli/PostTool.java index 97751490e3f4..bf8832ad6785 100644 --- a/solr/core/src/java/org/apache/solr/cli/PostTool.java +++ b/solr/core/src/java/org/apache/solr/cli/PostTool.java @@ -1342,6 +1342,11 @@ protected Set getLinksFromWebPage(URL url, InputStream is, String type, URI } } + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } + /** Utility class to hold the result form a page fetch */ public static class PageFetcherResult { int httpStatus = 200; diff --git a/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java b/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java index e1d002cf21ee..d6c120be041f 100644 --- a/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java +++ b/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java @@ -1079,6 +1079,11 @@ protected void copyIfNeeded(Path src, Path dest) throws IOException { throw new IllegalStateException("Required file " + dest.toAbsolutePath() + " not found!"); } + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } + protected boolean isPortAvailable(int port) { try (Socket s = new Socket("localhost", port)) { assert s != null; // To allow compilation. diff --git a/solr/core/src/java/org/apache/solr/cli/SnapshotCreateTool.java b/solr/core/src/java/org/apache/solr/cli/SnapshotCreateTool.java index dfc39bf7cb2d..893d57e4c6c7 100644 --- a/solr/core/src/java/org/apache/solr/cli/SnapshotCreateTool.java +++ b/solr/core/src/java/org/apache/solr/cli/SnapshotCreateTool.java @@ -97,4 +97,9 @@ public void createSnapshot(SolrClient solrClient, String collectionName, String + e.getLocalizedMessage()); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/SnapshotDeleteTool.java b/solr/core/src/java/org/apache/solr/cli/SnapshotDeleteTool.java index 00b5c3c01979..5092d87b68f6 100644 --- a/solr/core/src/java/org/apache/solr/cli/SnapshotDeleteTool.java +++ b/solr/core/src/java/org/apache/solr/cli/SnapshotDeleteTool.java @@ -97,4 +97,9 @@ public void deleteSnapshot(SolrClient solrClient, String collectionName, String + e.getLocalizedMessage()); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/SnapshotDescribeTool.java b/solr/core/src/java/org/apache/solr/cli/SnapshotDescribeTool.java index 477ad265e7d5..3b5102d50796 100644 --- a/solr/core/src/java/org/apache/solr/cli/SnapshotDescribeTool.java +++ b/solr/core/src/java/org/apache/solr/cli/SnapshotDescribeTool.java @@ -136,4 +136,9 @@ private Collection listCollectionSnapshots( return result; } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/SnapshotExportTool.java b/solr/core/src/java/org/apache/solr/cli/SnapshotExportTool.java index d71b8df8b1a2..6b2406fd80e2 100644 --- a/solr/core/src/java/org/apache/solr/cli/SnapshotExportTool.java +++ b/solr/core/src/java/org/apache/solr/cli/SnapshotExportTool.java @@ -131,4 +131,9 @@ public void exportSnapshot( + e.getLocalizedMessage()); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/SnapshotListTool.java b/solr/core/src/java/org/apache/solr/cli/SnapshotListTool.java index 4501fddc8397..eedad418a1c0 100644 --- a/solr/core/src/java/org/apache/solr/cli/SnapshotListTool.java +++ b/solr/core/src/java/org/apache/solr/cli/SnapshotListTool.java @@ -85,4 +85,9 @@ public void listSnapshots(SolrClient solrClient, String collectionName) { + e.getLocalizedMessage()); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java index 9f34c32cb371..6a6828161ff9 100755 --- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java +++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java @@ -42,9 +42,11 @@ import org.apache.commons.cli.help.HelpFormatter; import org.apache.commons.cli.help.TableDefinition; import org.apache.commons.cli.help.TextHelpAppendable; +import org.apache.solr.client.api.util.SolrVersion; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest; import org.apache.solr.common.util.ContentStreamBase; +import org.apache.solr.common.util.EnvUtils; import org.apache.solr.common.util.NamedList; import org.apache.solr.common.util.SuppressForbidden; import org.apache.solr.util.configuration.SSLConfigurationsFactory; @@ -52,14 +54,92 @@ import org.slf4j.LoggerFactory; /** Command-line utility for working with Solr. */ +@picocli.CommandLine.Command( + name = "solr", + version = "Apache Solr version " + SolrVersion.LATEST_STRING, + mixinStandardHelpOptions = true, + commandListHeading = "\nCommands:\n", + descriptionHeading = "Global options:\n", + footer = { + "", + "SolrCloud example (embedded Zookeeper):", + "", + " ./solr start", + "", + "Get help for a command by running 'solr COMMAND --help'.", + "", + "For more help on how to use Solr, head to https://solr.apache.org/" + }, + usageHelpAutoWidth = true, + usageHelpWidth = 120, + defaultValueProvider = CliDefaultValueProvider.class, + subcommands = { + StartCommand.class, + StopCommand.class, + StatusTool.class, + VersionTool.class, + ZkTool.class + }) public class SolrCLI implements CLIO { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + @SuppressForbidden(reason = "SolrCLI is a CLI entry point; System.exit is required here") + public static void exit(int exitStatus) { + try { + System.exit(exitStatus); + } catch (java.lang.SecurityException secExc) { + if (exitStatus != 0) + throw new RuntimeException("SolrCLI failed to exit with status " + exitStatus); + } + } + /** Runs a tool. */ public static void main(String[] args) throws Exception { - ToolRuntime runtime = new DefaultToolRuntime(); + if (EnvUtils.getPropertyAsBool("solr.picocli", false)) { + SSLConfigurationsFactory.current().init(); + picocli.CommandLine commandLine = new picocli.CommandLine(new SolrCLI()); + propagateCommandSettings(commandLine); + exit(commandLine.execute(args)); + } else { + exit(parseWithCommonsCli(args)); + } + } + + /** Propagates common settings to all subcommands. */ + private static void propagateCommandSettings(picocli.CommandLine cmd) { + for (picocli.CommandLine subcommand : cmd.getSubcommands().values()) { + subcommand.getCommandSpec().defaultValueProvider(cmd.getCommandSpec().defaultValueProvider()); + subcommand + .getCommandSpec() + .mixinStandardHelpOptions(cmd.getCommandSpec().mixinStandardHelpOptions()); + subcommand.getCommandSpec().usageMessage().width(cmd.getCommandSpec().usageMessage().width()); + subcommand + .getCommandSpec() + .usageMessage() + .autoWidth(cmd.getCommandSpec().usageMessage().autoWidth()); + subcommand + .getCommandSpec() + .usageMessage() + .commandListHeading(cmd.getCommandSpec().usageMessage().commandListHeading()); + subcommand + .getCommandSpec() + .usageMessage() + .footer( + "\nFor a full CLI reference, see https://solr.apache.org/guide/solr/latest/deployment-guide/solr-control-script-reference.html"); + propagateCommandSettings(subcommand); + } + } + /** + * Parses the command-line arguments passed by the user using Apache Commons CLI. This + * + * @param args the original command-line arguments + * @deprecated Please use picocli + */ + @Deprecated(since = "10.1") + public static int parseWithCommonsCli(String[] args) throws Exception { + ToolRuntime runtime = new DefaultToolRuntime(); final boolean hasNoCommand = args == null || args.length == 0 || args[0] == null || args[0].trim().isEmpty(); final boolean isHelpCommand = !hasNoCommand && Arrays.asList("-h", "--help").contains(args[0]); @@ -114,7 +194,7 @@ public static void main(String[] args) throws Exception { runtime.exit(1); } CommandLine cli = parseCmdLine(tool, args); - runtime.exit(tool.runTool(cli)); + return tool.runTool(cli); } public static Tool findTool(String[] args, ToolRuntime runtime) throws Exception { @@ -122,6 +202,7 @@ public static Tool findTool(String[] args, ToolRuntime runtime) throws Exception return newTool(toolType, runtime); } + @Deprecated public static CommandLine parseCmdLine(Tool tool, String[] args) throws IOException { // the parser doesn't like -D props List toolArgList = new ArrayList<>(); @@ -137,7 +218,7 @@ public static CommandLine parseCmdLine(Tool tool, String[] args) throws IOExcept String[] toolArgs = toolArgList.toArray(new String[0]); // process command-line args to configure this application - CommandLine cli = processCommandLineArgs(tool, toolArgs); + org.apache.commons.cli.CommandLine cli = processCommandLineArgs(tool, toolArgs); List argList = cli.getArgList(); argList.addAll(dashDList); @@ -176,6 +257,7 @@ protected static void checkSslStoreSysProp(String solrInstallDir, String key) { } // Creates an instance of the requested tool, using classpath scanning if necessary + @Deprecated private static Tool newTool(String toolType, ToolRuntime runtime) throws Exception { if ("healthcheck".equals(toolType)) return new HealthcheckTool(runtime); else if ("status".equals(toolType)) return new StatusTool(runtime); @@ -228,7 +310,7 @@ private static Tool newTool(String toolType, ToolRuntime runtime) throws Excepti * CLI option. */ public static String getOptionWithDeprecatedAndDefault( - CommandLine cli, Option opt, Option deprecated, String def) { + org.apache.commons.cli.CommandLine cli, Option opt, Option deprecated, String def) { String val = cli.getOptionValue(opt); if (val == null) { val = cli.getOptionValue(deprecated); @@ -238,6 +320,7 @@ public static String getOptionWithDeprecatedAndDefault( // TODO: SOLR-17429 - remove the custom logic when Commons CLI is upgraded and // makes stderr the default, or makes Option.toDeprecatedString() public. + @Deprecated private static void deprecatedHandlerStdErr(Option o) { // Deprecated options without a description act as "stealth" options if (o.isDeprecated() && !o.getDeprecated().getDescription().isBlank()) { @@ -252,11 +335,12 @@ private static void deprecatedHandlerStdErr(Option o) { } /** Parses the command-line arguments passed by the user. */ + @Deprecated public static CommandLine processCommandLineArgs(Tool tool, String[] args) throws IOException { Options options = tool.getOptions(); ToolRuntime runtime = tool.getRuntime(); - CommandLine cli = null; + org.apache.commons.cli.CommandLine cli = null; try { cli = DefaultParser.builder() @@ -293,6 +377,7 @@ public static CommandLine processCommandLineArgs(Tool tool, String[] args) throw } /** Prints tool help for a given tool */ + @Deprecated public static void printToolHelp(Tool tool) throws IOException { HelpFormatter formatter = getFormatter(); Options nonDeprecatedOptions = new Options(); @@ -311,6 +396,7 @@ public static void printToolHelp(Tool tool) throws IOException { autoGenerateUsage); } + @Deprecated @SuppressForbidden(reason = "System.out for formatting") public static HelpFormatter getFormatter() { TextHelpAppendable helpAppendable = @@ -346,7 +432,12 @@ public void appendParagraph(CharSequence paragraph) throws IOException { return formatter; } - /** Scans Jar files on the classpath for Tool implementations to activate. */ + /** + * Scans Jar files on the classpath for Tool implementations to activate. + * + * @deprecated With Picocli we no longer need to scan the classpath for Tool implementations? + */ + @Deprecated private static List> findToolClassesInPackage(String packageName) { List> toolClasses = new ArrayList<>(); try { @@ -370,6 +461,7 @@ private static List> findToolClassesInPackage(String packa return toolClasses; } + @Deprecated private static Set findClasses(String path, String packageName) throws Exception { Set classes = new TreeSet<>(); if (path.startsWith("file:") && path.contains("!")) { @@ -428,6 +520,7 @@ public static String uptime(long uptimeMs) { numSeconds); } + @Deprecated private static void printHelp() { print("Usage: solr COMMAND OPTIONS"); diff --git a/solr/core/src/java/org/apache/solr/cli/StartCommand.java b/solr/core/src/java/org/apache/solr/cli/StartCommand.java new file mode 100644 index 000000000000..b7c0b3f07c5b --- /dev/null +++ b/solr/core/src/java/org/apache/solr/cli/StartCommand.java @@ -0,0 +1,228 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.cli; + +import java.util.concurrent.Callable; +import picocli.CommandLine; + +@CommandLine.Command( + name = "start", + description = "Starts Solr in standalone or SolrCloud mode.", + mixinStandardHelpOptions = true) +public class StartCommand implements Callable { + + @CommandLine.Spec CommandLine.Model.CommandSpec spec; + + @CommandLine.Option( + names = {"-f", "--foreground"}, + description = + "Start Solr in foreground; default is background with logs to solr-PORT-console.log") + boolean foreground; + + @CommandLine.Option( + names = "--user-managed", + description = "Start Solr in standalone mode. Default is SolrCloud (ZooKeeper) mode.") + boolean userManaged; + + @CommandLine.Option(names = "--host", description = "Specify the hostname for this Solr instance") + String host; + + @CommandLine.Option( + names = {"-p", "--port"}, + description = + "Specify the Solr HTTP port; default is 8983. STOP_PORT=($SOLR_PORT-1000), RMI_PORT=($SOLR_PORT+10000)") + String port; + + @CommandLine.Option( + names = "--server-dir", + description = "Specify the Solr server directory; default is 'server'") + String serverDir; + + @CommandLine.Option( + names = {"-z", "--zk-host"}, + description = + "Zookeeper connection string; default is to start an embedded ZooKeeper on PORT+10000") + String zkHost; + + @CommandLine.Option( + names = {"-m", "--memory"}, + description = "Set JVM heap size, e.g., -m 4g sets -Xms4g -Xmx4g; default is 512m") + String memory; + + @CommandLine.Option( + names = "--solr-home", + description = + "Set solr.solr.home system property; default is 'server/solr'. Ignored in examples mode") + String solrHome; + + @CommandLine.Option( + names = "--data-home", + description = + "Set solr.data.home system property for index data storage; default is solr.solr.home") + String dataHome; + + @CommandLine.Option( + names = {"-e", "--example"}, + description = "Run an example: cloud, techproducts, schemaless, films") + String example; + + @CommandLine.Option( + names = "--jvm-opts", + description = "Additional JVM parameters, e.g., --jvm-opts \"-verbose:gc\"") + String jvmOpts; + + @CommandLine.Option( + names = {"-j", "--jettyconfig"}, + description = + "Additional Jetty parameters, e.g., -j \"--include-jetty-dir=/etc/jetty/custom/server/\"") + String jettyParams; + + @CommandLine.Option( + names = {"-y", "--no-prompt"}, + description = "Don't prompt for input; accept all defaults when running examples") + boolean noPrompt; + + @CommandLine.Option( + names = "--prompt-inputs", + description = + "Don't prompt for input; comma-delimited list of inputs to use when running examples that accept user input", + paramLabel = "") + String promptInputs; + + @CommandLine.Option( + names = "--example-dir", + description = + "Override the directory containing example configurations used when running examples with --example") + String exampleDir; + + @CommandLine.Option( + names = "--force", + description = "Override warning when attempting to start Solr as root user") + boolean force; + + @CommandLine.Option( + names = {"--verbose"}, + description = "Set log level to DEBUG (verbose); default is INFO") + boolean verbose; + + @CommandLine.Option( + names = {"--quiet", "-q"}, + description = "Set log level to WARN (quiet); default is INFO") + boolean quiet; + + @CommandLine.Option( + names = "--fullhelp", + description = "Print detailed help with full option descriptions", + hidden = true) + boolean fullhelp; + + @Override + public Integer call() { + if (fullhelp) { + printFullHelp(); + } + // Actual start logic is handled by the bin/solr shell script. + return 0; + } + + private void printFullHelp() { + CLIO.out( + """ + Usage: solr start [OPTIONS] + + Starts Solr in standalone or SolrCloud mode. + + Options: + -f, --foreground + Start Solr in foreground; default starts Solr in the background and sends + stdout / stderr to solr-PORT-console.log + + --user-managed + Start Solr in user managed aka standalone mode. + See: https://solr.apache.org/guide/solr/latest/deployment-guide/cluster-types.html + + --host + Specify the hostname for this Solr instance + + -p, --port + Specify the port to start the Solr HTTP listener on; default is 8983. + The specified port (SOLR_PORT) will also be used to determine the stop port: + STOP_PORT=($SOLR_PORT-1000) and JMX RMI listen port RMI_PORT=($SOLR_PORT+10000). + For instance, if you set -p 8985, then STOP_PORT=7985 and RMI_PORT=18985 + + --server-dir + Specify the Solr server directory; defaults to server + + -z, --zk-host + Zookeeper connection string; ignored in User Managed (--user-managed) mode. + If neither ZK_HOST is defined in solr.in.sh nor -z is specified, an embedded + ZooKeeper instance will be launched. + Set ZK_CREATE_CHROOT=true if your ZK host has a chroot path to create it automatically. + + -m, --memory + Sets the min (-Xms) and max (-Xmx) heap size for the JVM, e.g., -m 4g sets + -Xms4g -Xmx4g; by default, this script sets the heap size to 512m + + --solr-home + Sets the solr.solr.home system property; Solr will create core directories here. + Allows running multiple Solr instances on the same host while reusing the same + server directory. If set, the directory should contain a solr.xml file unless + solr.xml exists in ZooKeeper. Ignored when running examples (-e). + Default: server/solr + + --data-home + Sets the solr.data.home system property, where Solr stores index data in + /data subdirectories. If not set, Solr uses solr.solr.home. + + -e, --example + Name of the example to run; available examples: + cloud: SolrCloud example + techproducts: Comprehensive example illustrating many of Solr's core capabilities + schemaless: Schema-less example (schema inferred from data during indexing) + films: Example starting with _default configset with explicit fields + + --jvm-opts + Additional parameters to pass to the JVM when starting Solr, e.g., to enable + a Java debugger: --jvm-opts "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=18983" + Wrap additional parameters in double quotes. + + -j, --jettyconfig + Additional parameters to pass to Jetty, e.g., to add a config folder: + -j "--include-jetty-dir=/etc/jetty/custom/server/" + Wrap additional parameters in double quotes. + + -y, --no-prompt + Don't prompt for input; accept all defaults when running examples. + + --prompt-inputs + Don't prompt for input; comma-delimited list of inputs for examples that + accept user input. + + --example-dir + Override the directory containing example configurations. + + --force + Override warning when attempting to start Solr as the root user. + + --verbose + Set log level to DEBUG (verbose); default is INFO + + -q, --quiet + Set log level to WARN (quiet); default is INFO + """); + } +} diff --git a/solr/core/src/java/org/apache/solr/cli/StatusTool.java b/solr/core/src/java/org/apache/solr/cli/StatusTool.java index ca9391fca18b..bbe253a6887f 100644 --- a/solr/core/src/java/org/apache/solr/cli/StatusTool.java +++ b/solr/core/src/java/org/apache/solr/cli/StatusTool.java @@ -44,8 +44,39 @@ * *

Get the status of a Solr server. */ +@picocli.CommandLine.Command( + name = "status", + mixinStandardHelpOptions = true, + description = "Get the status of a Solr server.") public class StatusTool extends ToolBase { + @picocli.CommandLine.Option( + names = {"--max-wait-secs"}, + description = "Wait up to the specified number of seconds to see Solr running.") + private Integer maxWaitSecs; + @picocli.CommandLine.Option( + names = {"-p", "--port"}, + description = "Port on localhost to check status for") + private Integer port; + + @picocli.CommandLine.Option( + names = {"-s", "--solr-url"}, + description = "Base Solr URL, which can be used to determine the zk-host if that's not known") + private String solrUrl; + + @picocli.CommandLine.Option( + names = {"--short"}, + paramLabel = "short", + description = "Short format. Prints one URL per line for running instances") + private boolean shortFormat; + + @picocli.CommandLine.Option( + names = {"-u", "--credentials"}, + description = + "Credentials in the format username:password. Example: --credentials solr:SolrRocks") + private String credentials; + + @Deprecated private static final Option MAX_WAIT_SECS_OPTION = Option.builder() .longOpt("max-wait-secs") @@ -56,6 +87,7 @@ public class StatusTool extends ToolBase { .desc("Wait up to the specified number of seconds to see Solr running.") .get(); + @Deprecated public static final Option PORT_OPTION = Option.builder("p") .longOpt("port") @@ -65,6 +97,7 @@ public class StatusTool extends ToolBase { .desc("Port on localhost to check status for") .get(); + @Deprecated public static final Option SHORT_OPTION = Option.builder() .longOpt("short") @@ -74,6 +107,10 @@ public class StatusTool extends ToolBase { private final SolrProcessManager processMgr; + public StatusTool() { + this(new DefaultToolRuntime()); + } + public StatusTool(ToolRuntime runtime) { super(runtime); processMgr = new SolrProcessManager(); @@ -98,11 +135,15 @@ public Options getOptions() { @Override public void runImpl(CommandLine cli) throws Exception { - String solrUrl = cli.getOptionValue(CommonCLIOptions.SOLR_URL_OPTION); - Integer port = cli.hasOption(PORT_OPTION) ? cli.getParsedOptionValue(PORT_OPTION) : null; - boolean shortFormat = cli.hasOption(SHORT_OPTION); - int maxWaitSecs = cli.getParsedOptionValue(MAX_WAIT_SECS_OPTION, 0); + solrUrl = cli.getOptionValue(CommonCLIOptions.SOLR_URL_OPTION); + port = cli.hasOption(PORT_OPTION) ? cli.getParsedOptionValue(PORT_OPTION) : null; + shortFormat = cli.hasOption(SHORT_OPTION); + maxWaitSecs = cli.getParsedOptionValue(MAX_WAIT_SECS_OPTION, 0); + + runTool(); + } + public int runTool() throws Exception { if (solrUrl != null) { if (!URLUtil.hasScheme(solrUrl)) { CLIO.err("Invalid URL provided: " + solrUrl); @@ -113,14 +154,14 @@ public void runImpl(CommandLine cli) throws Exception { if (maxWaitSecs > 0) { // Used by Windows start script when starting Solr try { - waitForSolrUpAndPrintStatus(solrUrl, cli, maxWaitSecs); + waitForSolrUpAndPrintStatus(solrUrl); runtime.exit(0); } catch (Exception e) { CLIO.err(e.getMessage()); runtime.exit(1); } } else { - boolean running = printStatusFromRunningSolr(solrUrl, cli); + boolean running = printStatusFromRunningSolr(solrUrl); runtime.exit(running ? 0 : 1); } } @@ -135,7 +176,7 @@ public void runImpl(CommandLine cli) throws Exception { if (shortFormat) { CLIO.out(solrUrl); } else { - printProcessStatus(proc.get(), cli); + printProcessStatus(proc.get()); } runtime.exit(0); } @@ -148,7 +189,7 @@ public void runImpl(CommandLine cli) throws Exception { if (shortFormat) { CLIO.out(process.getLocalUrl()); } else { - printProcessStatus(process, cli); + printProcessStatus(process); } } } else { @@ -156,17 +197,16 @@ public void runImpl(CommandLine cli) throws Exception { CLIO.out("\nNo Solr nodes are running.\n"); } } + return 0; } - private void printProcessStatus(SolrProcess process, CommandLine cli) throws Exception { - int maxWaitSecs = cli.getParsedOptionValue(MAX_WAIT_SECS_OPTION, 0); - boolean shortFormat = cli.hasOption(SHORT_OPTION); + private void printProcessStatus(SolrProcess process) throws Exception { String pidUrl = process.getLocalUrl(); if (shortFormat) { CLIO.out(pidUrl); } else { if (maxWaitSecs > 0) { - waitForSolrUpAndPrintStatus(pidUrl, cli, maxWaitSecs); + waitForSolrUpAndPrintStatus(pidUrl); } else { CLIO.out( String.format( @@ -174,23 +214,22 @@ private void printProcessStatus(SolrProcess process, CommandLine cli) throws Exc "\nSolr process %s running on port %s", process.pid(), process.port())); - printStatusFromRunningSolr(pidUrl, cli); + printStatusFromRunningSolr(pidUrl); } } CLIO.out(""); } - public void waitForSolrUpAndPrintStatus(String solrUrl, CommandLine cli, int maxWaitSecs) - throws Exception { + public void waitForSolrUpAndPrintStatus(String pidUrl) throws Exception { int solrPort = -1; try { - solrPort = CLIUtils.portFromUrl(solrUrl); + solrPort = CLIUtils.portFromUrl(pidUrl); } catch (Exception e) { CLIO.err("Invalid URL provided, does not contain port"); runtime.exit(1); } echo("Waiting up to " + maxWaitSecs + " seconds to see Solr running on port " + solrPort); - boolean solrUp = waitForSolrUp(solrUrl, cli, maxWaitSecs); + boolean solrUp = waitForSolrUp(pidUrl); if (solrUp) { echo("Started Solr server on port " + solrPort + ". Happy searching!"); } else { @@ -202,35 +241,28 @@ public void waitForSolrUpAndPrintStatus(String solrUrl, CommandLine cli, int max /** * Wait for Solr to come online and return true if it does, false otherwise. * - * @param solrUrl the URL of the Solr server - * @param cli the command line options - * @param maxWaitSecs the maximum number of seconds to wait * @return true if Solr comes online, false otherwise */ - public boolean waitForSolrUp(String solrUrl, CommandLine cli, int maxWaitSecs) throws Exception { + public boolean waitForSolrUp(String pidUrl) throws Exception { try { - waitToSeeSolrUp( - solrUrl, - cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION), - maxWaitSecs, - TimeUnit.SECONDS); + waitToSeeSolrUp(pidUrl, credentials, maxWaitSecs, TimeUnit.SECONDS); return true; } catch (TimeoutException timeout) { return false; } } - public boolean printStatusFromRunningSolr(String solrUrl, CommandLine cli) { + public boolean printStatusFromRunningSolr(String pidUrl) { String statusJson = null; try { - statusJson = statusFromRunningSolr(solrUrl, cli); + statusJson = statusFromRunningSolr(pidUrl); } catch (Exception e) { /* ignore */ } if (statusJson != null) { runtime.println(statusJson); } else { - CLIO.err("Solr at " + solrUrl + " not online."); + CLIO.err("Solr at " + pidUrl + " not online."); } return statusJson != null; } @@ -238,16 +270,13 @@ public boolean printStatusFromRunningSolr(String solrUrl, CommandLine cli) { /** * Get the status of a Solr server and responds with a JSON status string. * - * @param solrUrl the URL of the Solr server - * @param cli the command line options * @return the status of the Solr server or null if the server is not online * @throws Exception if there is an error getting the status */ - public String statusFromRunningSolr(String solrUrl, CommandLine cli) throws Exception { + public String statusFromRunningSolr(String pidUrl) throws Exception { try { CharArr arr = new CharArr(); - new JSONWriter(arr, 2) - .write(getStatus(solrUrl, cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION))); + new JSONWriter(arr, 2).write(getStatus(pidUrl)); return arr.toString(); } catch (Exception exc) { if (CLIUtils.exceptionIsAuthRelated(exc)) { @@ -257,19 +286,23 @@ public String statusFromRunningSolr(String solrUrl, CommandLine cli) throws Exce // this is not actually an error from the tool as it's ok if Solr is not online. return null; } else { - throw new Exception("Failed to get system information from " + solrUrl + " due to: " + exc); + throw new Exception("Failed to get system information from " + pidUrl + " due to: " + exc); } } } + public Map waitToSeeSolrUp(String pidUrl) throws Exception { + return waitToSeeSolrUp(pidUrl, credentials, maxWaitSecs, TimeUnit.SECONDS); + } + @SuppressWarnings("BusyWait") public Map waitToSeeSolrUp( - String solrUrl, String credentials, long maxWait, TimeUnit unit) throws Exception { + String pidUrl, String credentials, long maxWait, TimeUnit unit) throws Exception { long timeout = System.nanoTime() + TimeUnit.NANOSECONDS.convert(maxWait, unit); while (System.nanoTime() < timeout) { try { - return getStatus(solrUrl, credentials); + return getStatus(pidUrl); } catch (Exception exc) { if (CLIUtils.exceptionIsAuthRelated(exc)) { throw exc; @@ -289,8 +322,12 @@ public Map waitToSeeSolrUp( + " seconds!"); } - public Map getStatus(String solrUrl, String credentials) throws Exception { - try (var solrClient = CLIUtils.getSolrClient(solrUrl, credentials)) { + public Map getStatus(String pidUrl) throws Exception { + return getStatus(pidUrl, credentials); + } + + public Map getStatus(String pidUrl, String credentials) throws Exception { + try (var solrClient = CLIUtils.getSolrClient(pidUrl, credentials)) { return reportStatus(solrClient); } } @@ -340,4 +377,9 @@ private static Map getCloudStatus(SolrClient solrClient, String return cloudStatus; } + + @Override + public int callTool() throws Exception { + return runTool(); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/StopCommand.java b/solr/core/src/java/org/apache/solr/cli/StopCommand.java new file mode 100644 index 000000000000..2dd4a762dfe0 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/cli/StopCommand.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.cli; + +import picocli.CommandLine; + +/** + * This class is currently only used for printing CLI usage. + * The stop logic is currently handled in start script. + */ +@CommandLine.Command(name = "stop", description = "Stops Solr.", mixinStandardHelpOptions = true) +public class StopCommand { + + @CommandLine.Option( + names = {"-p", "--port"}, + description = + "Specify the port the Solr HTTP listener is bound to.\n" + + "The STOP_PORT is derived as ($SOLR_PORT-1000).") + String port; + + @CommandLine.Option( + names = {"-k", "--key"}, + description = "Stop key; default is solrrocks", + defaultValue = "solrrocks") + String key; + + @CommandLine.Option( + names = "--all", + description = "Find and stop all running Solr servers on this host") + boolean all; + + @CommandLine.Option( + names = {"--verbose"}, + description = "Enable verbose mode.") + boolean verbose; +} diff --git a/solr/core/src/java/org/apache/solr/cli/StreamTool.java b/solr/core/src/java/org/apache/solr/cli/StreamTool.java index 61d95c79d418..a0830d9919a4 100644 --- a/solr/core/src/java/org/apache/solr/cli/StreamTool.java +++ b/solr/core/src/java/org/apache/solr/cli/StreamTool.java @@ -480,6 +480,11 @@ static String listToString(List values, String internalDelim) { return buf.toString(); } + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } + static String readExpression(LineNumberReader bufferedReader, String[] args) throws IOException { StringBuilder exprBuff = new StringBuilder(); diff --git a/solr/core/src/java/org/apache/solr/cli/Tool.java b/solr/core/src/java/org/apache/solr/cli/Tool.java index 1ad58bce50e7..acaf9c62d31b 100644 --- a/solr/core/src/java/org/apache/solr/cli/Tool.java +++ b/solr/core/src/java/org/apache/solr/cli/Tool.java @@ -22,6 +22,7 @@ public interface Tool { /** Defines the interface to a Solr tool that can be run from this command-line app. */ + @Deprecated String getName(); /** @@ -30,6 +31,7 @@ public interface Tool { * * @return The custom usage string or 'null' to auto generate (default) */ + @Deprecated default String getUsage() { return null; } @@ -37,6 +39,7 @@ default String getUsage() { /** * Optional header to display before the options in help output. Defaults to 'List of options:' */ + @Deprecated default String getHeader() { return "List of options:"; } @@ -45,6 +48,7 @@ default String getHeader() { * Optional footer to display after the options in help output. Defaults to a link to reference * guide */ + @Deprecated default String getFooter() { return "\nPlease see the Reference Guide for more tools documentation: https://solr.apache.org/guide/solr/latest/deployment-guide/solr-control-script-reference.html"; } @@ -57,7 +61,9 @@ default String getFooter() { * * @return The {@link Options} this tool supports. */ + @Deprecated Options getOptions(); + @Deprecated int runTool(CommandLine cli) throws Exception; } diff --git a/solr/core/src/java/org/apache/solr/cli/ToolBase.java b/solr/core/src/java/org/apache/solr/cli/ToolBase.java index 9d3734c3d931..8b3d4304cf3c 100644 --- a/solr/core/src/java/org/apache/solr/cli/ToolBase.java +++ b/solr/core/src/java/org/apache/solr/cli/ToolBase.java @@ -18,18 +18,21 @@ package org.apache.solr.cli; import com.fasterxml.jackson.core.JsonProcessingException; +import java.util.concurrent.Callable; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.OptionGroup; import org.apache.commons.cli.Options; import org.apache.solr.client.solrj.request.json.JacksonContentWriter; import org.apache.solr.util.StartupLoggingUtils; -public abstract class ToolBase implements Tool { +public abstract class ToolBase implements Tool, Callable { + @picocli.CommandLine.Option( + names = {"-v", "--verbose"}, + description = "Enable verbose mode.") + private boolean verbose = false; protected final ToolRuntime runtime; - private boolean verbose = false; - protected ToolBase(ToolRuntime runtime) { this.runtime = runtime; } @@ -75,6 +78,7 @@ public ToolRuntime getRuntime() { * * @return OptionGroup validates that only one option is supplied by the caller. */ + @Deprecated public OptionGroup getConnectionOptions() { OptionGroup optionGroup = new OptionGroup(); optionGroup.addOption(CommonCLIOptions.SOLR_URL_OPTION); @@ -112,5 +116,36 @@ private void raiseLogLevelUnlessVerbose() { } } + @Deprecated public abstract void runImpl(CommandLine cli) throws Exception; + + /** + * Called by picocli to execute the tool's logic. Each tool must implement this method to support + * the picocli-based invocation path. + */ + public abstract int callTool() throws Exception; + + /** Called by picocli for a tool invocation. Delegates to {@link #callTool()}. */ + @Override + public Integer call() { + raiseLogLevelUnlessVerbose(); + + int toolExitStatus = 0; + try { + toolExitStatus = callTool(); + } catch (Exception exc) { + // since this is a CLI, spare the user the stacktrace + String excMsg = exc.getMessage(); + if (excMsg != null) { + CLIO.err("\nERROR: " + excMsg + "\n"); + if (verbose) { + exc.printStackTrace(CLIO.getErrStream()); + } + toolExitStatus = 1; + } else { + throw new RuntimeException(exc); + } + } + return toolExitStatus; + } } diff --git a/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java b/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java index 021f05d7e82c..0639a9ce9969 100644 --- a/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java +++ b/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java @@ -71,4 +71,9 @@ public void runImpl(CommandLine cli) throws Exception { zkClient.updateACLs(path); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/VersionTool.java b/solr/core/src/java/org/apache/solr/cli/VersionTool.java index b8ae826ebc6a..f8f8393dbde5 100644 --- a/solr/core/src/java/org/apache/solr/cli/VersionTool.java +++ b/solr/core/src/java/org/apache/solr/cli/VersionTool.java @@ -20,8 +20,13 @@ import org.apache.commons.cli.CommandLine; import org.apache.solr.client.api.util.SolrVersion; +@picocli.CommandLine.Command(name = "version", description = "Prints the Solr version.") public class VersionTool extends ToolBase { + public VersionTool() { + this(new DefaultToolRuntime()); + } + public VersionTool(ToolRuntime runtime) { super(runtime); } @@ -33,6 +38,16 @@ public String getName() { @Override public void runImpl(CommandLine cli) throws Exception { + printVersion(); + } + + @Override + public int callTool() throws Exception { + printVersion(); + return 0; + } + + private void printVersion() { CLIO.out("Solr version is: " + SolrVersion.LATEST); } } diff --git a/solr/core/src/java/org/apache/solr/cli/ZkConnectionOptions.java b/solr/core/src/java/org/apache/solr/cli/ZkConnectionOptions.java new file mode 100644 index 000000000000..432d8cddd03e --- /dev/null +++ b/solr/core/src/java/org/apache/solr/cli/ZkConnectionOptions.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.cli; + +import java.util.Map; +import org.apache.solr.client.solrj.SolrClient; +import picocli.CommandLine; + +/** + * Picocli mixin providing common ZooKeeper connection options shared across ZK sub-commands. + * + *

Use {@code @CommandLine.Mixin ZkConnectionOptions zkOpts} in a command class to inherit these + * options. Call {@link #resolveZkHost()} to obtain a resolved ZooKeeper connection string, applying + * the same fallback logic as the commons-cli path. + */ +public class ZkConnectionOptions { + + @CommandLine.Option( + names = {"-z", "--zk-host"}, + description = + "Zookeeper connection string; unnecessary if ZK_HOST is defined in solr.in.sh; otherwise, defaults to " + + CommonCLIOptions.DefaultValues.ZK_HOST + + '.') + public String zkHost; + + @CommandLine.Option( + names = {"-s", "--solr-url"}, + description = + "Base Solr URL, which can be used to determine the zk-host if --zk-host is not known") + public String solrUrl; + + @CommandLine.Option( + names = {"-u", "--credentials"}, + description = + "Credentials in the format username:password. Example: --credentials solr:SolrRocks") + public String credentials; + + /** + * Resolves the ZooKeeper connection string using the following precedence: + * + *

    + *
  1. Explicit {@code --zk-host} option value + *
  2. ZooKeeper host derived by querying the Solr instance at {@code --solr-url} + *
  3. ZooKeeper host derived by querying the default Solr URL ({@code http://localhost:8983}), + * with a warning printed to stderr + *
+ * + * @return resolved ZooKeeper connection string, never null + * @throws IllegalStateException if the Solr instance is not running in SolrCloud mode + * @throws Exception if the Solr instance cannot be reached + */ + public String resolveZkHost() throws Exception { + if (zkHost != null) { + return zkHost; + } + + String resolvedSolrUrl = solrUrl; + if (resolvedSolrUrl == null) { + resolvedSolrUrl = CLIUtils.getDefaultSolrUrl(); + CLIO.err( + "Neither --zk-host or --solr-url parameters, nor ZK_HOST env var provided, so assuming solr url is " + + resolvedSolrUrl + + "."); + } + + try (SolrClient solrClient = CLIUtils.getSolrClient(resolvedSolrUrl, credentials)) { + Map status = StatusTool.reportStatus(solrClient); + @SuppressWarnings("unchecked") + Map cloud = (Map) status.get("cloud"); + if (cloud != null) { + String zookeeper = (String) cloud.get("ZooKeeper"); + if (zookeeper != null && zookeeper.endsWith("(embedded)")) { + zookeeper = zookeeper.substring(0, zookeeper.length() - "(embedded)".length()); + } + if (zookeeper != null) { + return zookeeper; + } + } + } + + throw new IllegalStateException( + "Solr at " + + resolvedSolrUrl + + " is not running in SolrCloud mode. Cannot use zk commands."); + } +} diff --git a/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java b/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java index 4bf9a2663156..3e7f912f2485 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java @@ -207,4 +207,9 @@ public void runImpl(CommandLine cli) throws Exception { throw (e); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java b/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java index 2ca4be286f20..cd09844e05c1 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java @@ -17,16 +17,39 @@ package org.apache.solr.cli; import java.lang.invoke.MethodHandles; +import java.util.concurrent.TimeUnit; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Options; +import org.apache.solr.client.solrj.impl.SolrZkClientTimeout; import org.apache.solr.common.cloud.SolrZkClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** Supports zk ls command in the bin/solr script. */ +@picocli.CommandLine.Command( + name = "ls", + mixinStandardHelpOptions = true, + description = "List the contents of a ZooKeeper node.") public class ZkLsTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + @picocli.CommandLine.Mixin ZkConnectionOptions zkOpts; + + @picocli.CommandLine.Parameters( + index = "0", + arity = "1", + description = "The path of the ZooKeeper znode path to list.") + private String path; + + @picocli.CommandLine.Option( + names = {"-r", "--recursive"}, + description = "Apply the command recursively.") + private boolean recursive; + + public ZkLsTool() { + this(new DefaultToolRuntime()); + } + public ZkLsTool(ToolRuntime runtime) { super(runtime); } @@ -57,7 +80,6 @@ public void runImpl(CommandLine cli) throws Exception { try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, zkHost)) { echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ..."); - boolean recursive = cli.hasOption(CommonCLIOptions.RECURSIVE_OPTION); echoIfVerbose( "Getting listing for ZooKeeper node " @@ -72,4 +94,33 @@ public void runImpl(CommandLine cli) throws Exception { throw (e); } } + + private void doLs(SolrZkClient zkClient) throws Exception { + echoIfVerbose("\nConnecting to ZooKeeper at " + zkOpts.zkHost + " ..."); + echoIfVerbose( + "Getting listing for ZooKeeper node " + + path + + " from ZooKeeper at " + + zkOpts.zkHost + + " recursive: " + + recursive); + runtime.print(zkClient.listZnode(path, recursive)); + } + + @Override + public int callTool() throws Exception { + String zkHost = zkOpts.resolveZkHost(); + + try (SolrZkClient zkClient = + new SolrZkClient.Builder() + .withUrl(zkHost) + .withTimeout(SolrZkClientTimeout.DEFAULT_ZK_CLIENT_TIMEOUT, TimeUnit.MILLISECONDS) + .build()) { + doLs(zkClient); + return 0; + } catch (Exception e) { + log.error("Could not complete ls operation for reason: ", e); + throw (e); + } + } } diff --git a/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java b/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java index 2a644f2a1364..5c0d51a30c19 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java @@ -85,4 +85,9 @@ public void runImpl(CommandLine cli) throws Exception { throw (e); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java b/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java index a11ac0884f87..759eacb1b7b9 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java @@ -98,4 +98,9 @@ public void runImpl(CommandLine cli) throws Exception { throw (e); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java b/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java index 92e6b4daaa4e..8aab732c6f8b 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java @@ -84,4 +84,9 @@ public void runImpl(CommandLine cli) throws Exception { throw (e); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/ZkTool.java b/solr/core/src/java/org/apache/solr/cli/ZkTool.java new file mode 100644 index 000000000000..af69c2461e25 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/cli/ZkTool.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.cli; + +import java.util.concurrent.Callable; +import picocli.CommandLine; + +/** + * Sub commands for working with ZooKeeper, only here to provide a common parent for the subcommands + * and print tool help. + */ +@CommandLine.Command( + name = "zk", + mixinStandardHelpOptions = true, + description = "Sub commands for working with ZooKeeper.", + subcommands = {ZkLsTool.class}) +public class ZkTool implements Callable { + + @CommandLine.Spec CommandLine.Model.CommandSpec spec; + + @Override + public Integer call() { + spec.commandLine().usage(CLIO.getOutStream()); + return 0; + } +} diff --git a/solr/core/src/java/org/apache/solr/cli/ZkToolHelp.java b/solr/core/src/java/org/apache/solr/cli/ZkToolHelp.java index 9251484a859f..8217702f511f 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkToolHelp.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkToolHelp.java @@ -77,4 +77,9 @@ public void runImpl(CommandLine cli) throws Exception { "Pass --help or -h after any COMMAND to see command-specific usage information such as: ./solr zk ls --help"); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/test-framework/build.gradle b/solr/test-framework/build.gradle index a7bac2b63bd8..78d2ed3592fe 100644 --- a/solr/test-framework/build.gradle +++ b/solr/test-framework/build.gradle @@ -67,6 +67,10 @@ dependencies { implementation libs.dropwizard.metrics.core implementation libs.dropwizard.metrics.jetty12.ee10 implementation libs.commonscli.commonscli + permitUnusedDeclared libs.commonscli.commonscli + implementation libs.picocli + permitUnusedDeclared libs.picocli + annotationProcessor libs.picocli.codegen implementation libs.apache.httpcomponents.httpclient implementation libs.apache.httpcomponents.httpcore implementation libs.opentelemetry.api diff --git a/solr/test-framework/gradle.lockfile b/solr/test-framework/gradle.lockfile index b97f50c3595d..6ff709116263 100644 --- a/solr/test-framework/gradle.lockfile +++ b/solr/test-framework/gradle.lockfile @@ -31,10 +31,11 @@ com.google.protobuf:protobuf-java:3.25.8=annotationProcessor,errorprone,testAnno com.j256.simplemagic:simplemagic:1.17=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath com.jayway.jsonpath:json-path:2.9.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath com.tdunning:t-digest:3.3=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -commons-cli:commons-cli:1.10.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +commons-cli:commons-cli:1.10.0=apiHelper,compileClasspath,jarValidation,permitUnusedDeclared,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-codec:commons-codec:1.19.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.20.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -info.picocli:picocli:4.7.6=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath +info.picocli:picocli-codegen:4.7.6=annotationProcessor +info.picocli:picocli:4.7.6=annotationProcessor,apiHelper,compileClasspath,jarValidation,permitUnusedDeclared,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath