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
 }


Reply via email to