This is an automated email from the ASF dual-hosted git repository.
janhoy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/main by this push:
new 2391f49b1e4 SOLR-17450 StatusTool with pure Java code (#2712)
2391f49b1e4 is described below
commit 2391f49b1e47b2cecee147332e57fef9d5ee7583
Author: Jan Høydahl <[email protected]>
AuthorDate: Mon Oct 21 00:20:39 2024 +0200
SOLR-17450 StatusTool with pure Java code (#2712)
Co-authored-by: Christos Malliaridis <[email protected]>
---
.../randomization/policies/solr-tests.policy | 6 +
solr/bin/solr | 50 +---
solr/bin/solr.cmd | 30 +--
.../core/src/java/org/apache/solr/cli/SolrCLI.java | 3 +-
.../org/apache/solr/cli/SolrProcessManager.java | 243 ++++++++++++++++++++
.../src/java/org/apache/solr/cli/StatusTool.java | 252 ++++++++++++++++-----
.../apache/solr/cli/SolrProcessManagerTest.java | 196 ++++++++++++++++
solr/packaging/test/test_help.bats | 4 +-
solr/packaging/test/test_status.bats | 30 ++-
9 files changed, 672 insertions(+), 142 deletions(-)
diff --git a/gradle/testing/randomization/policies/solr-tests.policy
b/gradle/testing/randomization/policies/solr-tests.policy
index e5c37b4c912..0a7fea95ad6 100644
--- a/gradle/testing/randomization/policies/solr-tests.policy
+++ b/gradle/testing/randomization/policies/solr-tests.policy
@@ -109,6 +109,8 @@ grant {
permission java.lang.RuntimePermission "writeFileDescriptor";
// needed by hadoop http
permission java.lang.RuntimePermission "getProtectionDomain";
+ // SolrProcessMgr to list processes
+ permission java.lang.RuntimePermission "manageProcess";
// These two *have* to be spelled out a separate
permission java.lang.management.ManagementPermission "control";
@@ -250,6 +252,10 @@ grant {
// expanded to a wildcard if set, allows all networking everywhere
permission java.net.SocketPermission "${solr.internal.network.permission}",
"accept,listen,connect,resolve";
+
+ // Run java
+ permission java.io.FilePermission "${java.home}${/}-", "execute";
+ permission java.io.FilePermission "C:\\Windows\\*\\wmic.exe", "execute";
};
// Grant all permissions to Gradle test runner classes.
diff --git a/solr/bin/solr b/solr/bin/solr
index 8a993233a0a..c4dccba4ef4 100755
--- a/solr/bin/solr
+++ b/solr/bin/solr
@@ -493,55 +493,13 @@ function run_tool() {
# shellcheck disable=SC2086
"$JAVA" $SOLR_SSL_OPTS $AUTHC_OPTS ${SOLR_ZK_CREDS_AND_ACLS:-}
${SOLR_TOOL_OPTS:-} -Dsolr.install.dir="$SOLR_TIP" \
-
-Dlog4j.configurationFile="$DEFAULT_SERVER_DIR/resources/log4j2-console.xml" \
+
-Dlog4j.configurationFile="$DEFAULT_SERVER_DIR/resources/log4j2-console.xml"
-Dsolr.pid.dir="$SOLR_PID_DIR" \
-classpath
"$DEFAULT_SERVER_DIR/solr-webapp/webapp/WEB-INF/lib/*:$DEFAULT_SERVER_DIR/lib/ext/*:$DEFAULT_SERVER_DIR/lib/*"
\
org.apache.solr.cli.SolrCLI "$@"
return $?
} # end run_tool function
-# get status about any Solr nodes running on this host
-function get_status() {
- # first, see if Solr is running
- numSolrs=$(find "$SOLR_PID_DIR" -name "solr-*.pid" -type f | wc -l | tr -d '
')
- if [ "$numSolrs" != "0" ]; then
- echo -e "\nFound $numSolrs Solr nodes: "
- while read PIDF
- do
- ID=$(cat "$PIDF")
- port=$(jetty_port "$ID")
- if [ "$port" != "" ]; then
- echo -e "\nSolr process $ID running on port $port"
- run_tool status --solr-url
"$SOLR_URL_SCHEME://$SOLR_TOOL_HOST:$port" "$@"
- echo ""
- else
- echo -e "\nSolr process $ID from $PIDF not found."
- fi
- done < <(find "$SOLR_PID_DIR" -name "solr-*.pid" -type f)
- else
- # no pid files but check using ps just to be sure
- numSolrs=$(ps auxww | grep start\.jar | grep solr\.solr\.home | grep -v
grep | wc -l | sed -e 's/^[ \t]*//')
- if [ "$numSolrs" != "0" ]; then
- echo -e "\nFound $numSolrs Solr nodes: "
- PROCESSES=$(ps auxww | grep start\.jar | grep solr\.solr\.home | grep -v
grep | awk '{print $2}' | sort -r)
- for ID in $PROCESSES
- do
- port=$(jetty_port "$ID")
- if [ "$port" != "" ]; then
- echo ""
- echo "Solr process $ID running on port $port"
- run_tool status --solr-url
"$SOLR_URL_SCHEME://$SOLR_TOOL_HOST:$port" "$@"
- echo ""
- fi
- done
- else
- echo -e "\nNo Solr nodes are running.\n"
- run_tool status "$@"
- fi
- fi
-
-} # end get_status
-
# tries to gracefully stop Solr using the Jetty
# stop command and if that fails, then uses kill -9
# (will attempt to thread dump before killing)
@@ -632,12 +590,6 @@ else
exit
fi
-# status tool
-if [ "$SCRIPT_CMD" == "status" ]; then
- get_status
- exit $?
-fi
-
# configure authentication
if [[ "$SCRIPT_CMD" == "auth" ]]; then
: "${SOLR_SERVER_DIR:=$DEFAULT_SERVER_DIR}"
diff --git a/solr/bin/solr.cmd b/solr/bin/solr.cmd
index 846433022c5..94a973236b1 100755
--- a/solr/bin/solr.cmd
+++ b/solr/bin/solr.cmd
@@ -253,7 +253,7 @@ IF "%1"=="-h" goto run_solrcli
IF "%1"=="--help" goto run_solrcli
IF "%1"=="-help" goto run_solrcli
IF "%1"=="/?" goto run_solrcli
-IF "%1"=="status" goto get_status
+IF "%1"=="status" goto run_solrcli
IF "%1"=="version" goto run_solrcli
IF "%1"=="-v" goto run_solrcli
IF "%1"=="-version" goto run_solrcli
@@ -1208,34 +1208,6 @@ REM Run the requested example
REM End of run_example
goto done
-:get_status
-REM Find all Java processes, correlate with those listening on a port
-REM and then try to contact via that port using the status tool
-for /f "usebackq" %%i in (`dir /b "%SOLR_TIP%\bin" ^| findstr /i
"^solr-.*\.port$"`) do (
- set SOME_SOLR_PORT=
- For /F "Delims=" %%J In ('type "%SOLR_TIP%\bin\%%i"') do set
SOME_SOLR_PORT=%%~J
- if NOT "!SOME_SOLR_PORT!"=="" (
- for /f "tokens=2,5" %%j in ('netstat -aon ^| find "TCP " ^| find ":0 " ^|
find ":!SOME_SOLR_PORT! "') do (
- IF NOT "%%k"=="0" (
- if "%%j"=="%SOLR_JETTY_HOST%:!SOME_SOLR_PORT!" (
- @echo.
- set has_info=1
- echo Found Solr process %%k running on port !SOME_SOLR_PORT!
- REM Passing in %2 (-h or --help) directly is captured by a custom
help path for usage output
- "%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
"%DEFAULT_SERVER_DIR%\solr-webapp\webapp\WEB-INF\lib\*;%DEFAULT_SERVER_DIR%\lib\ext\*"
^
- org.apache.solr.cli.SolrCLI status --solr-url
!SOLR_URL_SCHEME!://%SOLR_TOOL_HOST%:!SOME_SOLR_PORT! %2
- @echo.
- )
- )
- )
- )
-)
-if NOT "!has_info!"=="1" echo No running Solr nodes found.
-set has_info=
-goto done
-
:run_solrcli
"%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"
^
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 eac28a424e2..98c22c7f6d8 100755
--- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
+++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java
@@ -424,7 +424,8 @@ public class SolrCLI implements CLIO {
// TODO: SOLR-17429 - remove the custom logic when Commons CLI is upgraded
and
// makes stderr the default, or makes Option.toDeprecatedString() public.
private static void deprecatedHandlerStdErr(Option o) {
- if (o.isDeprecated()) {
+ // Deprecated options without a description act as "stealth" options
+ if (o.isDeprecated() && !o.getDeprecated().getDescription().isBlank()) {
final StringBuilder buf =
new StringBuilder().append("Option
'-").append(o.getOpt()).append('\'');
if (o.getLongOpt() != null) {
diff --git a/solr/core/src/java/org/apache/solr/cli/SolrProcessManager.java
b/solr/core/src/java/org/apache/solr/cli/SolrProcessManager.java
new file mode 100644
index 00000000000..42b38b2e7a8
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/cli/SolrProcessManager.java
@@ -0,0 +1,243 @@
+/*
+ * 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.servlet.SolrDispatchFilter.SOLR_INSTALL_DIR_ATTRIBUTE;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.lang.invoke.MethodHandles;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.apache.lucene.util.Constants;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.EnvUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Class to interact with Solr OS processes */
+public class SolrProcessManager {
+ private static final Logger log =
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private final Map<Long, SolrProcess> pidProcessMap;
+ private final Map<Integer, SolrProcess> portProcessMap;
+ private final Path pidDir;
+ private static final Pattern pidFilePattern =
Pattern.compile("^solr-([0-9]+)\\.(pid|port)$");
+ // Set this to true during testing to allow the SolrProcessManager to find
only mock Solr
+ // processes
+ public static boolean enableTestingMode = false;
+
+ public SolrProcessManager() {
+ pidProcessMap =
+ ProcessHandle.allProcesses()
+ .filter(p -> p.info().command().orElse("").contains("java"))
+ .filter(p -> commandLine(p).orElse("").contains("-Djetty.port="))
+ .filter(
+ p -> !enableTestingMode ||
commandLine(p).orElse("").contains("-DmockSolr=true"))
+ .collect(
+ Collectors.toUnmodifiableMap(
+ ProcessHandle::pid,
+ ph ->
+ new SolrProcess(
+ ph.pid(), parsePortFromProcess(ph).orElseThrow(),
isProcessSsl(ph))));
+ portProcessMap =
+ pidProcessMap.values().stream().collect(Collectors.toUnmodifiableMap(p
-> p.port, p -> p));
+ String solrInstallDir = EnvUtils.getProperty(SOLR_INSTALL_DIR_ATTRIBUTE);
+ pidDir =
+ Paths.get(
+ EnvUtils.getProperty(
+ "solr.pid.dir",
+ solrInstallDir != null
+ ? solrInstallDir + "/bin"
+ : System.getProperty("java.io.tmpdir")));
+ }
+
+ public boolean isRunningWithPort(Integer port) {
+ return portProcessMap.containsKey(port);
+ }
+
+ public boolean isRunningWithPid(Long pid) {
+ return pidProcessMap.containsKey(pid);
+ }
+
+ public Optional<SolrProcess> processForPort(Integer port) {
+ return portProcessMap.containsKey(port)
+ ? Optional.of(portProcessMap.get(port))
+ : Optional.empty();
+ }
+
+ /** Return the SolrProcess for a given PID, if it is running */
+ public Optional<SolrProcess> getProcessForPid(Long pid) {
+ return pidProcessMap.containsKey(pid) ?
Optional.of(pidProcessMap.get(pid)) : Optional.empty();
+ }
+
+ /**
+ * Scans the PID directory for Solr PID files and returns a list of
SolrProcesses for each running
+ * Solr instance. If a PID file is found but no process is running, the PID
file is deleted. On
+ * Windows, the file is a 'PORT' file containing the port number.
+ *
+ * @return a list of SolrProcesses for each running Solr instance
+ */
+ public Collection<SolrProcess> scanSolrPidFiles() throws IOException {
+ List<SolrProcess> processes = new ArrayList<>();
+ try (Stream<Path> pidFiles =
+ Files.list(pidDir)
+ .filter(p ->
pidFilePattern.matcher(p.getFileName().toString()).matches())) {
+ for (Path p : pidFiles.collect(Collectors.toList())) {
+ Optional<SolrProcess> process;
+ if (p.toString().endsWith(".port")) {
+ // On Windows, the file is a 'PORT' file containing the port number.
+ Integer port = Integer.valueOf(Files.readAllLines(p).get(0));
+ process = processForPort(port);
+ } else {
+ // On Linux, the file is a 'PID' file containing the process ID.
+ Long pid = Long.valueOf(Files.readAllLines(p).get(0));
+ process = getProcessForPid(pid);
+ }
+ if (process.isPresent()) {
+ processes.add(process.get());
+ } else {
+ log.warn("PID file {} found, but no process running. Deleting PID
file", p.getFileName());
+ Files.deleteIfExists(p);
+ }
+ }
+ return processes;
+ }
+ }
+
+ public Collection<SolrProcess> getAllRunning() {
+ return pidProcessMap.values();
+ }
+
+ private Optional<Integer> parsePortFromProcess(ProcessHandle ph) {
+ Optional<String> portStr =
+ arguments(ph).stream()
+ .filter(a -> a.contains("-Djetty.port="))
+ .map(s -> s.split("=")[1])
+ .findFirst();
+ return portStr.isPresent() ? portStr.map(Integer::parseInt) :
Optional.empty();
+ }
+
+ private boolean isProcessSsl(ProcessHandle ph) {
+ return arguments(ph).stream()
+ .anyMatch(
+ arg -> List.of("--module=https", "--module=ssl",
"--module=ssl-reload").contains(arg));
+ }
+
+ /**
+ * Gets the command line of a process as a string. This is a workaround for
the fact that
+ * ProcessHandle.info().command() is not (yet) implemented on Windows.
+ *
+ * @param ph the process handle
+ * @return the command line of the process
+ */
+ private static Optional<String> commandLine(ProcessHandle ph) {
+ if (!Constants.WINDOWS) {
+ return ph.info().commandLine();
+ } else {
+ long desiredProcessid = ph.pid();
+ try {
+ Process process =
+ new ProcessBuilder(
+ "wmic",
+ "process",
+ "where",
+ "ProcessID=" + desiredProcessid,
+ "get",
+ "commandline",
+ "/format:list")
+ .redirectErrorStream(true)
+ .start();
+ try (InputStreamReader inputStreamReader =
+ new InputStreamReader(process.getInputStream(),
StandardCharsets.UTF_8);
+ BufferedReader reader = new BufferedReader(inputStreamReader)) {
+ while (true) {
+ String line = reader.readLine();
+ if (line == null) {
+ return Optional.empty();
+ }
+ if (!line.startsWith("CommandLine=")) {
+ continue;
+ }
+ return Optional.of(line.substring("CommandLine=".length()));
+ }
+ }
+ } catch (IOException e) {
+ throw new SolrException(
+ SolrException.ErrorCode.SERVER_ERROR,
+ "Error getting command line for process " + desiredProcessid,
+ e);
+ }
+ }
+ }
+
+ /**
+ * Gets the arguments of a process as a list of strings. With workaround for
Windows.
+ *
+ * @param ph the process handle
+ * @return the arguments of the process
+ */
+ private static List<String> arguments(ProcessHandle ph) {
+ if (!Constants.WINDOWS) {
+ return Arrays.asList(ph.info().arguments().orElse(new String[] {}));
+ } else {
+ return Arrays.asList(commandLine(ph).orElse("").split("\\s+"));
+ }
+ }
+
+ /** Represents a running Solr process */
+ public static class SolrProcess {
+ private final long pid;
+ private final int port;
+ private final boolean isHttps;
+
+ public SolrProcess(long pid, int port, boolean isHttps) {
+ this.pid = pid;
+ this.port = port;
+ this.isHttps = isHttps;
+ }
+
+ public long getPid() {
+ return pid;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public boolean isHttps() {
+ return isHttps;
+ }
+
+ public String getLocalUrl() {
+ return String.format(Locale.ROOT, "%s://localhost:%s/solr", isHttps ?
"https" : "http", port);
+ }
+ }
+}
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 5b9df1570d7..94f46b106dd 100644
--- a/solr/core/src/java/org/apache/solr/cli/StatusTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/StatusTool.java
@@ -17,23 +17,29 @@
package org.apache.solr.cli;
+import static org.apache.solr.cli.SolrCLI.OPTION_SOLRURL;
+
import java.io.PrintStream;
import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
+import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.commons.cli.CommandLine;
-import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
-import org.apache.commons.cli.Options;
+import org.apache.solr.cli.SolrProcessManager.SolrProcess;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.request.GenericSolrRequest;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.URLUtil;
import org.noggit.CharArr;
import org.noggit.JSONWriter;
@@ -43,12 +49,15 @@ import org.noggit.JSONWriter;
* <p>Get the status of a Solr server.
*/
public class StatusTool extends ToolBase {
+ private final SolrProcessManager processMgr;
+
public StatusTool() {
this(CLIO.getOutStream());
}
public StatusTool(PrintStream stdout) {
super(stdout);
+ processMgr = new SolrProcessManager();
}
@Override
@@ -62,75 +71,214 @@ public class StatusTool extends ToolBase {
.argName("SECS")
.hasArg()
.required(false)
+ .deprecated() // Will make it a stealth option, not printed or
complained about
.desc("Wait up to the specified number of seconds to see Solr
running.")
.build();
+ public static final Option OPTION_PORT =
+ Option.builder("p")
+ .longOpt("port")
+ .argName("PORT")
+ .required(false)
+ .hasArg()
+ .desc("Port on localhost to check status for")
+ .build();
+
+ public static final Option OPTION_SHORT =
+ Option.builder()
+ .longOpt("short")
+ .argName("SHORT")
+ .required(false)
+ .desc("Short format. Prints one URL per line for running instances")
+ .build();
+
@Override
public List<Option> getOptions() {
- return List.of(
- // The solr-url option is not exposed to the end user, and is
- // created by the bin/solr script and passed into this command
directly,
- // therefore we don't use the SolrCLI.OPTION_SOLRURL.
- Option.builder()
- .argName("URL")
- .longOpt("solr-url")
- .hasArg()
- .required(false)
- .desc("Property set by calling scripts, not meant for user
configuration.")
- .build(),
- OPTION_MAXWAITSECS);
+ return List.of(OPTION_SOLRURL, OPTION_MAXWAITSECS, OPTION_PORT,
OPTION_SHORT);
}
@Override
public void runImpl(CommandLine cli) throws Exception {
- // Override the default help behaviour to put out a customized message
that only list user
- // settable Options.
- if ((cli.getOptions().length == 0 && cli.getArgs().length == 0)
- || cli.hasOption("h")
- || cli.hasOption("help")) {
- final Options options = new Options();
- options.addOption(OPTION_MAXWAITSECS);
- new HelpFormatter().printHelp("status", options);
- return;
+ String solrUrl = cli.getOptionValue(OPTION_SOLRURL);
+ Integer port =
+ cli.hasOption(OPTION_PORT) ?
Integer.parseInt(cli.getOptionValue(OPTION_PORT)) : null;
+ boolean shortFormat = cli.hasOption(OPTION_SHORT);
+ int maxWaitSecs = Integer.parseInt(cli.getOptionValue("max-wait-secs",
"0"));
+
+ if (port != null && solrUrl != null) {
+ throw new IllegalArgumentException("Only one of port or url can be
specified");
}
- int maxWaitSecs = Integer.parseInt(cli.getOptionValue("max-wait-secs",
"0"));
- String solrUrl = SolrCLI.normalizeSolrUrl(cli);
- if (maxWaitSecs > 0) {
- int solrPort = new URI(solrUrl).getPort();
- echo("Waiting up to " + maxWaitSecs + " seconds to see Solr running on
port " + solrPort);
- try {
- waitToSeeSolrUp(
- solrUrl,
- cli.getOptionValue(SolrCLI.OPTION_CREDENTIALS.getLongOpt()),
- maxWaitSecs,
- TimeUnit.SECONDS);
- echo("Started Solr server on port " + solrPort + ". Happy searching!");
- } catch (TimeoutException timeout) {
- throw new Exception(
- "Solr at " + solrUrl + " did not come online within " +
maxWaitSecs + " seconds!");
+ if (solrUrl != null) {
+ if (!URLUtil.hasScheme(solrUrl)) {
+ CLIO.err("Invalid URL provided: " + solrUrl);
+ System.exit(1);
}
- } else {
- try {
- CharArr arr = new CharArr();
- new JSONWriter(arr, 2)
- .write(getStatus(solrUrl,
cli.getOptionValue(SolrCLI.OPTION_CREDENTIALS.getLongOpt())));
- echo(arr.toString());
- } catch (Exception exc) {
- if (SolrCLI.exceptionIsAuthRelated(exc)) {
- throw exc;
+
+ // URL provided, do not consult local processes, as the URL may be remote
+ if (maxWaitSecs > 0) {
+ // Used by Windows start script when starting Solr
+ try {
+ waitForSolrUpAndPrintStatus(solrUrl, cli, maxWaitSecs);
+ System.exit(0);
+ } catch (Exception e) {
+ CLIO.err(e.getMessage());
+ System.exit(1);
+ }
+ } else {
+ boolean running = printStatusFromRunningSolr(solrUrl, cli);
+ System.exit(running ? 0 : 1);
+ }
+ }
+
+ if (port != null) {
+ Optional<SolrProcess> proc = processMgr.processForPort(port);
+ if (proc.isEmpty()) {
+ CLIO.err("Could not find a running Solr on port " + port);
+ System.exit(1);
+ } else {
+ solrUrl = proc.get().getLocalUrl();
+ if (shortFormat) {
+ CLIO.out(solrUrl);
+ } else {
+ printProcessStatus(proc.get(), cli);
}
- if (SolrCLI.checkCommunicationError(exc)) {
- // this is not actually an error from the tool as it's ok if Solr is
not online.
- CLIO.err("Solr at " + solrUrl + " not online.");
+ System.exit(0);
+ }
+ }
+
+ // No URL or port, scan for running processes
+ Collection<SolrProcess> procs = processMgr.scanSolrPidFiles();
+ if (!procs.isEmpty()) {
+ for (SolrProcess process : procs) {
+ if (shortFormat) {
+ CLIO.out(process.getLocalUrl());
} else {
- throw new Exception(
- "Failed to get system information from " + solrUrl + " due to: "
+ exc);
+ printProcessStatus(process, cli);
}
}
+ } else {
+ if (!shortFormat) {
+ CLIO.out("\nNo Solr nodes are running.\n");
+ }
+ }
+ }
+
+ private void printProcessStatus(SolrProcess process, CommandLine cli) throws
Exception {
+ int maxWaitSecs = Integer.parseInt(cli.getOptionValue("max-wait-secs",
"0"));
+ boolean shortFormat = cli.hasOption(OPTION_SHORT);
+ String pidUrl = process.getLocalUrl();
+ if (shortFormat) {
+ CLIO.out(pidUrl);
+ } else {
+ if (maxWaitSecs > 0) {
+ waitForSolrUpAndPrintStatus(pidUrl, cli, maxWaitSecs);
+ } else {
+ CLIO.out(
+ String.format(
+ Locale.ROOT,
+ "\nSolr process %s running on port %s",
+ process.getPid(),
+ process.getPort()));
+ printStatusFromRunningSolr(pidUrl, cli);
+ }
+ }
+ CLIO.out("");
+ }
+
+ private Integer portFromUrl(String solrUrl) {
+ try {
+ URI uri = new URI(solrUrl);
+ int port = uri.getPort();
+ if (port == -1) {
+ return uri.getScheme().equals("https") ? 443 : 80;
+ } else {
+ return port;
+ }
+ } catch (URISyntaxException e) {
+ CLIO.err("Invalid URL provided, does not contain port");
+ System.exit(1);
+ return null;
+ }
+ }
+
+ public void waitForSolrUpAndPrintStatus(String solrUrl, CommandLine cli, int
maxWaitSecs)
+ throws Exception {
+ int solrPort = portFromUrl(solrUrl);
+ echo("Waiting up to " + maxWaitSecs + " seconds to see Solr running on
port " + solrPort);
+ boolean solrUp = waitForSolrUp(solrUrl, cli, maxWaitSecs);
+ if (solrUp) {
+ echo("Started Solr server on port " + solrPort + ". Happy searching!");
+ } else {
+ throw new Exception(
+ "Solr at " + solrUrl + " did not come online within " + maxWaitSecs
+ " seconds!");
+ }
+ }
+
+ /**
+ * 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 {
+ try {
+ waitToSeeSolrUp(
+ solrUrl,
+ cli.getOptionValue(SolrCLI.OPTION_CREDENTIALS.getLongOpt()),
+ maxWaitSecs,
+ TimeUnit.SECONDS);
+ return true;
+ } catch (TimeoutException timeout) {
+ return false;
+ }
+ }
+
+ public boolean printStatusFromRunningSolr(String solrUrl, CommandLine cli)
throws Exception {
+ String statusJson = null;
+ try {
+ statusJson = statusFromRunningSolr(solrUrl, cli);
+ } catch (Exception e) {
+ /* ignore */
+ }
+ if (statusJson != null) {
+ CLIO.out(statusJson);
+ } else {
+ CLIO.err("Solr at " + solrUrl + " not online.");
+ }
+ return statusJson != null;
+ }
+
+ /**
+ * 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 {
+ try {
+ CharArr arr = new CharArr();
+ new JSONWriter(arr, 2)
+ .write(getStatus(solrUrl,
cli.getOptionValue(SolrCLI.OPTION_CREDENTIALS.getLongOpt())));
+ return arr.toString();
+ } catch (Exception exc) {
+ if (SolrCLI.exceptionIsAuthRelated(exc)) {
+ throw exc;
+ }
+ if (SolrCLI.checkCommunicationError(exc)) {
+ // 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);
+ }
}
}
+ @SuppressWarnings("BusyWait")
public Map<String, Object> waitToSeeSolrUp(
String solrUrl, String credentials, long maxWait, TimeUnit unit) throws
Exception {
long timeout = System.nanoTime() + TimeUnit.NANOSECONDS.convert(maxWait,
unit);
diff --git a/solr/core/src/test/org/apache/solr/cli/SolrProcessManagerTest.java
b/solr/core/src/test/org/apache/solr/cli/SolrProcessManagerTest.java
new file mode 100644
index 00000000000..2bb49ef73ad
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/cli/SolrProcessManagerTest.java
@@ -0,0 +1,196 @@
+/*
+ * 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.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.invoke.MethodHandles;
+import java.net.ServerSocket;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Collectors;
+import org.apache.commons.math3.util.Pair;
+import org.apache.solr.SolrTestCase;
+import org.apache.solr.cli.SolrProcessManager.SolrProcess;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SolrProcessManagerTest extends SolrTestCase {
+ private static final Logger log =
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static SolrProcessManager solrProcessManager;
+ private static Pair<Integer, Process> processHttp;
+ private static Pair<Integer, Process> processHttps;
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ boolean isWindows = random().nextBoolean();
+ String PID_SUFFIX = isWindows ? ".port" : ".pid";
+ log.info("Simulating pid file on {}", isWindows ? "Windows" : "Linux");
+ processHttp = createProcess(findAvailablePort(), false);
+ processHttps = createProcess(findAvailablePort(), true);
+ long processHttpValue = isWindows ? processHttp.getKey() :
processHttp.getValue().pid();
+ long processHttpsValue = isWindows ? processHttps.getKey() :
processHttps.getValue().pid();
+ SolrProcessManager.enableTestingMode = true;
+ System.setProperty("jetty.port", Integer.toString(processHttp.getKey()));
+ Path pidDir = Files.createTempDirectory("solr-pid-dir").toAbsolutePath();
+ pidDir.toFile().deleteOnExit();
+ System.setProperty("solr.pid.dir", pidDir.toString());
+ Files.writeString(
+ pidDir.resolve("solr-" + processHttpValue + PID_SUFFIX),
Long.toString(processHttpValue));
+ Files.writeString(
+ pidDir.resolve("solr-" + processHttpsValue + PID_SUFFIX),
Long.toString(processHttpsValue));
+ Files.writeString(pidDir.resolve("solr-99999" + PID_SUFFIX), "99999"); //
Invalid
+ solrProcessManager = new SolrProcessManager();
+ }
+
+ @AfterClass
+ public static void afterClass() throws Exception {
+ processHttp.getValue().destroyForcibly();
+ processHttps.getValue().destroyForcibly();
+ SolrProcessManager.enableTestingMode = false;
+ System.clearProperty("jetty.port");
+ System.clearProperty("solr.pid.dir");
+ }
+
+ private static int findAvailablePort() throws IOException {
+ try (ServerSocket socket = new ServerSocket(0)) {
+ return socket.getLocalPort();
+ }
+ }
+
+ private static Pair<Integer, Process> createProcess(int port, boolean https)
throws IOException {
+ // Get the path to the java executable from the current JVM
+ String classPath =
+
Arrays.stream(System.getProperty("java.class.path").split(File.pathSeparator))
+ .filter(p -> p.contains("solr/core/build"))
+ .collect(Collectors.joining(File.pathSeparator));
+ ProcessBuilder processBuilder =
+ new ProcessBuilder(
+ System.getProperty("java.home") + "/bin/java",
+ "-Djetty.port=" + port,
+ "-DisHttps=" + https,
+ "-DmockSolr=true",
+ "-cp",
+ classPath,
+ "org.apache.solr.cli.SolrProcessManagerTest$MockSolrProcess",
+ https ? "--module=https" : "--module=http");
+
+ // Start the process and read first line of output
+ Process process = processBuilder.start();
+ try (InputStream is = process.getInputStream();
+ InputStreamReader isr = new InputStreamReader(is,
StandardCharsets.UTF_8);
+ BufferedReader br = new BufferedReader(isr)) {
+ System.out.println(br.readLine());
+ }
+ return new Pair<>(port, process);
+ }
+
+ public void testGetLocalUrl() {
+ assertFalse(solrProcessManager.getAllRunning().isEmpty());
+ solrProcessManager
+ .getAllRunning()
+ .forEach(
+ p ->
+ assertEquals(
+ (p.isHttps() ? "https" : "http") + "://localhost:" +
p.getPort() + "/solr",
+ p.getLocalUrl()));
+ }
+
+ public void testIsRunningWithPort() {
+ assertFalse(solrProcessManager.isRunningWithPort(0));
+ assertTrue(solrProcessManager.isRunningWithPort(processHttp.getKey()));
+ assertTrue(solrProcessManager.isRunningWithPort(processHttps.getKey()));
+ }
+
+ public void testIsRunningWithPid() {
+ assertFalse(solrProcessManager.isRunningWithPid(0L));
+
assertTrue(solrProcessManager.isRunningWithPid(processHttp.getValue().pid()));
+
assertTrue(solrProcessManager.isRunningWithPid(processHttps.getValue().pid()));
+ }
+
+ public void testProcessForPort() {
+ assertEquals(
+ processHttp.getKey().intValue(),
+
(solrProcessManager.processForPort(processHttp.getKey()).orElseThrow().getPort()));
+ assertEquals(
+ processHttps.getKey().intValue(),
+
(solrProcessManager.processForPort(processHttps.getKey()).orElseThrow().getPort()));
+ }
+
+ public void testGetProcessForPid() {
+ assertEquals(
+ processHttp.getValue().pid(),
+
(solrProcessManager.getProcessForPid(processHttp.getValue().pid()).orElseThrow().getPid()));
+ assertEquals(
+ processHttps.getValue().pid(),
+ (solrProcessManager
+ .getProcessForPid(processHttps.getValue().pid())
+ .orElseThrow()
+ .getPid()));
+ }
+
+ public void testScanSolrPidFiles() throws IOException {
+ Collection<SolrProcess> processes = solrProcessManager.scanSolrPidFiles();
+ assertEquals(2, processes.size());
+ }
+
+ public void testGetAllRunning() {
+ Collection<SolrProcess> processes = solrProcessManager.getAllRunning();
+ assertEquals(2, processes.size());
+ }
+
+ public void testSolrProcessMethods() {
+ SolrProcess http =
solrProcessManager.processForPort(processHttp.getKey()).orElseThrow();
+ assertEquals(processHttp.getValue().pid(), http.getPid());
+ assertEquals(processHttp.getKey().intValue(), http.getPort());
+ assertFalse(http.isHttps());
+ assertEquals("http://localhost:" + processHttp.getKey() + "/solr",
http.getLocalUrl());
+
+ SolrProcess https =
solrProcessManager.processForPort(processHttps.getKey()).orElseThrow();
+ assertEquals(processHttps.getValue().pid(), https.getPid());
+ assertEquals(processHttps.getKey().intValue(), https.getPort());
+ assertTrue(https.isHttps());
+ assertEquals("https://localhost:" + processHttps.getKey() + "/solr",
https.getLocalUrl());
+ }
+
+ /**
+ * This class is started as new java process by {@link
SolrProcessManagerTest#createProcess}, and
+ * it listens to a HTTP(s) port to simulate a real Solr process.
+ */
+ @SuppressWarnings("NewClassNamingConvention")
+ public static class MockSolrProcess {
+ public static void main(String[] args) {
+ int port = Integer.parseInt(System.getProperty("jetty.port"));
+ boolean https = System.getProperty("isHttps").equals("true");
+ try (ServerSocket serverSocket = new ServerSocket(port)) {
+ System.out.println("Listening on " + (https ? "https" : "http") + "
port " + port);
+ serverSocket.accept();
+ } catch (IOException e) {
+ System.err.println("Error listening to port: " + e.getMessage());
+ }
+ }
+ }
+}
diff --git a/solr/packaging/test/test_help.bats
b/solr/packaging/test/test_help.bats
index ee32052271a..54bab9fe85b 100644
--- a/solr/packaging/test/test_help.bats
+++ b/solr/packaging/test/test_help.bats
@@ -60,10 +60,8 @@ setup() {
@test "status help flag prints help" {
run solr status --help
- assert_output --partial 'usage: status'
+ assert_output --partial 'usage: bin/solr status'
refute_output --partial 'ERROR'
- # Make sure custom selection of options for status help works.
- refute_output --partial '--solr-url'
}
@test "healthcheck help flag prints help" {
diff --git a/solr/packaging/test/test_status.bats
b/solr/packaging/test/test_status.bats
index f599bc8e5e6..d1f8a53bcb4 100644
--- a/solr/packaging/test/test_status.bats
+++ b/solr/packaging/test/test_status.bats
@@ -33,22 +33,36 @@ teardown() {
assert_output --partial "No Solr nodes are running."
solr start
run solr status
- assert_output --partial "Found 1 Solr nodes:"
+ assert_output --partial "running on port ${SOLR_PORT}"
solr stop
run solr status
assert_output --partial "No Solr nodes are running."
+}
+@test "status with --solr-url from user" {
+ solr start
+ run solr status --solr-url http://localhost:${SOLR_PORT}
+ assert_output --partial "\"solr_home\":"
+ solr stop
}
-@test "status shell script ignores passed in --solr-url cli parameter from
user" {
+@test "status with --port from user" {
solr start
- run solr status --solr-url http://localhost:9999
- assert_output --partial "Found 1 Solr nodes:"
+ run solr status --port ${SOLR_PORT}
assert_output --partial "running on port ${SOLR_PORT}"
+ solr stop
}
-@test "status help flag outputs message highlighting not to use solr-url." {
- run solr status --help
- assert_output --partial 'usage: status'
- refute_output --partial 'ERROR'
+@test "status with invalid --solr-url from user" {
+ solr start
+ run solr status --solr-url http://invalidhost:${SOLR_PORT}
+ assert_output --partial "Solr at http://invalidhost:${SOLR_PORT} not online"
+ solr stop
+}
+
+@test "status with --short format" {
+ solr start
+ run solr status --port ${SOLR_PORT} --short
+ assert_output --partial "http://localhost:${SOLR_PORT}/solr"
+ solr stop
}