From ed3b268d62f23e212c410cb35aa4318afa088f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 20 Oct 2016 14:47:32 +0200 Subject: [PATCH] SOLR-99570: Various log tidying at Solr startup (cherry picked from commit 9776196) --- solr/CHANGES.txt | 15 +- solr/bin/solr | 18 +- solr/bin/solr.cmd | 28 +- .../java/org/apache/solr/util/SolrCLI.java | 271 +++++++++++++++++- .../org/apache/solr/util/UtilsToolTest.java | 185 ++++++++++++ 5 files changed, 479 insertions(+), 38 deletions(-) create mode 100644 solr/core/src/test/org/apache/solr/util/UtilsToolTest.java diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index 4e68c557603..1b1c9cbf226 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -42,8 +42,12 @@ Upgrade Notes prefix, then you will now get an error as these options are incompatible with numeric faceting. * Solr's logging verbosity at the INFO level has been greatly reduced, and - you may need to update the log configs to use the DEBUG level to get the - same logging messages as before. + you may need to update the log configs to use the DEBUG level to see all the + logging messages you used to see at INFO level before. + +* We are no longer backing up solr.log and solr_gc.log files in date-stamped copies forever. If you relied on + the solr_log_ or solr_gc_log_ being in the logs folder that will no longer be the case. + See SOLR-9570 for details. * The create/deleteCollection methods on MiniSolrCloudCluster have been deprecated. Clients should instead use the CollectionAdminRequest API. In @@ -272,6 +276,13 @@ Other Changes * SOLR-7850: Moved defaults within bin/solr.in.sh (and bin/solr.in.cmd on Windows) to bin/solr (and bin/solr.cmd) such that the default state of these files is to set nothing. This makes Solr work better with Docker. (David Smiley) +* SOLR-9570: Various log tidying now happens at Solr startup: + Old solr_log_ and solr_gc_log_ files are removed, avoiding disks to fill up, + solr.log.X files are rotated, preserving solr.log from last run in solr.log.1, solr.log.1 => solr.log.2 etc + solr-*-console.log files are moved into $SOLR_LOGS_DIR/archived/ instead of being overwritten + Last JVM garbage collection log solr_gc.log is moved into $SOLR_LOGS_DIR/archived/ + (janhoy) + ================== 6.2.1 ================== Bug Fixes diff --git a/solr/bin/solr b/solr/bin/solr index df6b4d03f43..6aa57095106 100755 --- a/solr/bin/solr +++ b/solr/bin/solr @@ -1387,20 +1387,10 @@ if [ ! -e "$SOLR_HOME" ]; then exit 1 fi -# backup the log files before starting -if [ -f "$SOLR_LOGS_DIR/solr.log" ]; then - if $verbose ; then - echo "Backing up $SOLR_LOGS_DIR/solr.log" - fi - mv "$SOLR_LOGS_DIR/solr.log" "$SOLR_LOGS_DIR/solr_log_$(date +"%Y%m%d_%H%M")" -fi - -if [ -f "$SOLR_LOGS_DIR/solr_gc.log" ]; then - if $verbose ; then - echo "Backing up $SOLR_LOGS_DIR/solr_gc.log" - fi - mv "$SOLR_LOGS_DIR/solr_gc.log" "$SOLR_LOGS_DIR/solr_gc_log_$(date +"%Y%m%d_%H%M")" -fi +run_tool utils -s "$DEFAULT_SERVER_DIR" -l "$SOLR_LOGS_DIR" -remove_old_solr_logs 7 || echo "Failed removing old solr logs" +run_tool utils -s "$DEFAULT_SERVER_DIR" -l "$SOLR_LOGS_DIR" -archive_gc_logs || echo "Failed archiving old GC logs" +run_tool utils -s "$DEFAULT_SERVER_DIR" -l "$SOLR_LOGS_DIR" -archive_console_logs || echo "Failed archiving old console logs" +run_tool utils -s "$DEFAULT_SERVER_DIR" -l "$SOLR_LOGS_DIR" -rotate_solr_logs 9 || echo "Failed rotating old solr logs" java_ver_out=`echo "$("$JAVA" -version 2>&1)"` JAVA_VERSION=`echo $java_ver_out | grep "java version" | awk '{ print substr($3, 2, length($3)-2); }'` diff --git a/solr/bin/solr.cmd b/solr/bin/solr.cmd index 10ea6d601cd..317a7890933 100644 --- a/solr/bin/solr.cmd +++ b/solr/bin/solr.cmd @@ -860,19 +860,11 @@ IF ERRORLEVEL 1 ( set IS_64bit=true ) -REM backup log files (use current timestamp for backup name) -For /f "tokens=2-4 delims=/ " %%a in ('date /t') do (set mydate=%%c-%%a-%%b) -For /f "tokens=1-2 delims=/:" %%a in ("%TIME%") do (set mytime=%%a%%b) -set now_ts=!mydate!_!mytime! -IF EXIST "!SOLR_LOGS_DIR!\solr.log" ( - echo Backing up !SOLR_LOGS_DIR!\solr.log - move /Y "!SOLR_LOGS_DIR!\solr.log" "!SOLR_LOGS_DIR!\solr_log_!now_ts!" -) - -IF EXIST "!SOLR_LOGS_DIR!\solr_gc.log" ( - echo Backing up !SOLR_LOGS_DIR!\solr_gc.log - move /Y "!SOLR_LOGS_DIR!\solr_gc.log" "!SOLR_LOGS_DIR!\solr_gc_log_!now_ts!" -) +REM Clean up and rotate logs +call :run_utils "-remove_old_solr_logs 7" || echo "Failed removing old solr logs" +call :run_utils "-archive_gc_logs" || echo "Failed archiving old GC logs" +call :run_utils "-archive_console_logs" || echo "Failed archiving old console logs" +call :run_utils "-rotate_solr_logs 9" || echo "Failed rotating old solr logs" IF NOT "%ZK_HOST%"=="" set SOLR_MODE=solrcloud @@ -1136,6 +1128,16 @@ goto done org.apache.solr.util.SolrCLI version goto done +:run_utils +set "TOOL_CMD=%~1" +"%JAVA%" %SOLR_SSL_OPTS% %SOLR_ZK_CREDS_AND_ACLS% -Dsolr.install.dir="%SOLR_TIP%" -Dlog4j.configuration="file:%DEFAULT_SERVER_DIR%\scripts\cloud-scripts\log4j.properties" ^ + -classpath "%DEFAULT_SERVER_DIR%\solr-webapp\webapp\WEB-INF\lib\*;%DEFAULT_SERVER_DIR%\lib\ext\*" ^ + org.apache.solr.util.SolrCLI utils -s "%DEFAULT_SERVER_DIR%" -l "%SOLR_LOGS_DIR%" %TOOL_CMD% +if errorlevel 1 ( + exit /b 1 +) +goto done + :parse_create_args IF [%1]==[] goto run_create IF "%1"=="-c" goto set_create_name diff --git a/solr/core/src/java/org/apache/solr/util/SolrCLI.java b/solr/core/src/java/org/apache/solr/util/SolrCLI.java index a60620e12b7..351638f2ac8 100644 --- a/solr/core/src/java/org/apache/solr/util/SolrCLI.java +++ b/solr/core/src/java/org/apache/solr/util/SolrCLI.java @@ -30,7 +30,10 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.nio.file.attribute.FileOwnerAttributeView; +import java.time.Instant; +import java.time.Period; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -45,6 +48,8 @@ import java.util.Set; import java.util.TreeSet; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -114,7 +119,6 @@ import static org.apache.solr.common.params.CommonParams.NAME; * Command-line utility for working with Solr. */ public class SolrCLI { - /** * Defines the interface to a Solr tool that can be run from this command-line app. */ @@ -235,7 +239,6 @@ public class SolrCLI { }; private static void exit(int exitStatus) { - // TODO: determine if we're running in a test and don't exit try { System.exit(exitStatus); } catch (java.lang.SecurityException secExc) { @@ -261,6 +264,17 @@ public class SolrCLI { exit(0); } + Tool tool = findTool(args); + CommandLine cli = parseCmdLine(args, tool.getOptions()); + System.exit(tool.runTool(cli)); + } + + public static Tool findTool(String[] args) throws Exception { + String toolType = args[0].trim().toLowerCase(Locale.ROOT); + return newTool(toolType); + } + + public static CommandLine parseCmdLine(String[] args, Option[] toolOptions) throws Exception { String configurerClassName = System.getProperty("solr.authentication.httpclient.configurer"); if (configurerClassName!=null) { try { @@ -274,10 +288,6 @@ public class SolrCLI { } } - // Determine the tool - String toolType = args[0].trim().toLowerCase(Locale.ROOT); - Tool tool = newTool(toolType); - // the parser doesn't like -D props List toolArgList = new ArrayList(); List dashDList = new ArrayList(); @@ -293,7 +303,7 @@ public class SolrCLI { // process command-line args to configure this application CommandLine cli = - processCommandLineArgs(joinCommonAndToolOptions(tool.getOptions()), toolArgs); + processCommandLineArgs(joinCommonAndToolOptions(toolOptions), toolArgs); List argList = cli.getArgList(); argList.addAll(dashDList); @@ -305,8 +315,7 @@ public class SolrCLI { checkSslStoreSysProp(solrInstallDir, "trustStore"); } - // run the tool - exit(tool.runTool(cli)); + return cli; } protected static void checkSslStoreSysProp(String solrInstallDir, String key) { @@ -370,6 +379,8 @@ public class SolrCLI { return new ZkLsTool(); else if ("assert".equals(toolType)) return new AssertTool(); + else if ("utils".equals(toolType)) + return new UtilsTool(); // If you add a built-in tool to this class, add it here to avoid // classpath scanning @@ -3342,4 +3353,246 @@ public class SolrCLI { } } } // end AssertTool class + + public static class UtilsTool extends ToolBase { + private Path serverPath; + private Path logsPath; + private boolean beQuiet; + + public UtilsTool() { this(System.out); } + public UtilsTool(PrintStream stdout) { super(stdout); } + + public String getName() { + return "prestart"; + } + + @SuppressWarnings("static-access") + public Option[] getOptions() { + return new Option[]{ + OptionBuilder + .withArgName("path") + .hasArg() + .withDescription("Path to server dir. Required if logs path is relative") + .create("s"), + OptionBuilder + .withArgName("path") + .hasArg() + .withDescription("Path to logs dir. If relative, also provide server dir with -s") + .create("l"), + OptionBuilder + .withDescription("Be quiet, don't print to stdout, only return exit codes") + .create("q"), + OptionBuilder + .withArgName("daysToKeep") + .hasArg() + .withType(Integer.class) + .withDescription("Path to logs directory") + .create("remove_old_solr_logs"), + OptionBuilder + .withArgName("generations") + .hasArg() + .withType(Integer.class) + .withDescription("Rotate solr.log to solr.log.1 etc") + .create("rotate_solr_logs"), + OptionBuilder + .withDescription("Archive old garbage collection logs into archive/") + .create("archive_gc_logs"), + OptionBuilder + .withDescription("Archive old console logs into archive/") + .create("archive_console_logs") + }; + } + + @Override + public int runTool(CommandLine cli) throws Exception { + if (cli.getOptions().length == 0 || cli.getArgs().length > 0 || cli.hasOption("h")) { + new HelpFormatter().printHelp("bin/solr utils [OPTIONS]", getToolOptions(this)); + return 1; + } + if (cli.hasOption("s")) { + serverPath = Paths.get(cli.getOptionValue("s")); + } + if (cli.hasOption("l")) { + logsPath = Paths.get(cli.getOptionValue("l")); + } + if (cli.hasOption("q")) { + beQuiet = cli.hasOption("q"); + } + if (cli.hasOption("remove_old_solr_logs")) { + if (removeOldSolrLogs(Integer.parseInt(cli.getOptionValue("remove_old_solr_logs"))) > 0) return 1; + } + if (cli.hasOption("rotate_solr_logs")) { + if (rotateSolrLogs(Integer.parseInt(cli.getOptionValue("rotate_solr_logs"))) > 0) return 1; + } + if (cli.hasOption("archive_gc_logs")) { + if (archiveGcLogs() > 0) return 1; + } + if (cli.hasOption("archive_console_logs")) { + if (archiveConsoleLogs() > 0) return 1; + } + return 0; + } + + /** + * Moves gc logs into archived/ + * @return 0 on success + * @throws Exception on failure + */ + public int archiveGcLogs() throws Exception { + prepareLogsPath(); + Path archivePath = logsPath.resolve("archived"); + if (!archivePath.toFile().exists()) { + Files.createDirectories(archivePath); + } + List archived = Files.find(archivePath, 1, (f, a) + -> a.isRegularFile() && String.valueOf(f.getFileName()).startsWith("solr_gc_")) + .collect(Collectors.toList()); + for (Path p : archived) { + Files.delete(p); + } + List files = Files.find(logsPath, 1, (f, a) + -> a.isRegularFile() && String.valueOf(f.getFileName()).startsWith("solr_gc_")) + .collect(Collectors.toList()); + if (files.size() > 0) { + out("Archiving " + files.size() + " old GC log files to " + archivePath); + for (Path p : files) { + Files.move(p, archivePath.resolve(p.getFileName()), StandardCopyOption.REPLACE_EXISTING); + } + } + return 0; + } + + /** + * Moves console log(s) into archiced/ + * @return 0 on success + * @throws Exception on failure + */ + public int archiveConsoleLogs() throws Exception { + prepareLogsPath(); + Path archivePath = logsPath.resolve("archived"); + if (!archivePath.toFile().exists()) { + Files.createDirectories(archivePath); + } + List archived = Files.find(archivePath, 1, (f, a) + -> a.isRegularFile() && String.valueOf(f.getFileName()).endsWith("-console.log")) + .collect(Collectors.toList()); + for (Path p : archived) { + Files.delete(p); + } + List files = Files.find(logsPath, 1, (f, a) + -> a.isRegularFile() && String.valueOf(f.getFileName()).endsWith("-console.log")) + .collect(Collectors.toList()); + if (files.size() > 0) { + out("Archiving " + files.size() + " console log files"); + for (Path p : files) { + Files.move(p, archivePath.resolve(p.getFileName()), StandardCopyOption.REPLACE_EXISTING); + } + } + return 0; + } + + /** + * Rotates solr.log before starting Solr. Mimics log4j2 behavior, i.e. with generations=9: + *
+     *   solr.log.9 (and higher) are deleted
+     *   solr.log.8 -> solr.log.9
+     *   solr.log.7 -> solr.log.8
+     *   ...
+     *   solr.log   -> solr.log.1
+     * 
+ * @param generations number of generations to keep. Should agree with setting in log4j.properties + * @return 0 if success + * @throws Exception if problems + */ + public int rotateSolrLogs(int generations) throws Exception { + prepareLogsPath(); + if (logsPath.toFile().exists() && logsPath.resolve("solr.log").toFile().exists()) { + out("Rotating solr logs, keeping a max of "+generations+" generations"); + try (Stream files = Files.find(logsPath, 1, + (f, a) -> a.isRegularFile() && String.valueOf(f.getFileName()).startsWith("solr.log.")) + .sorted((b,a) -> new Integer(a.getFileName().toString().substring(9)) + .compareTo(new Integer(b.getFileName().toString().substring(9))))) { + files.forEach(p -> { + try { + int number = Integer.parseInt(p.getFileName().toString().substring(9)); + if (number >= generations) { + Files.delete(p); + } else { + Path renamed = p.getParent().resolve("solr.log." + (number + 1)); + Files.move(p, renamed); + } + } catch (IOException e) { + out("Problem during rotation of log files: " + e.getMessage()); + } + }); + } catch (NumberFormatException nfe) { + throw new Exception("Do not know how to rotate solr.log. with non-numeric extension. Rotate aborted.", nfe); + } + Files.move(logsPath.resolve("solr.log"), logsPath.resolve("solr.log.1")); + } + + return 0; + } + + /** + * Deletes time-stamped old solr logs, if older than n days + * @param daysToKeep number of days logs to keep before deleting + * @return 0 on success + * @throws Exception on failure + */ + public int removeOldSolrLogs(int daysToKeep) throws Exception { + prepareLogsPath(); + if (logsPath.toFile().exists()) { + try (Stream stream = Files.find(logsPath, 2, (f, a) -> a.isRegularFile() + && Instant.now().minus(Period.ofDays(daysToKeep)).isAfter(a.lastModifiedTime().toInstant()) + && String.valueOf(f.getFileName()).startsWith("solr_log_"))) { + List files = stream.collect(Collectors.toList()); + if (files.size() > 0) { + out("Deleting "+files.size() + " solr_log_* files older than " + daysToKeep + " days."); + for (Path p : files) { + Files.delete(p); + } + } + } + } + return 0; + } + + // Private methods to follow + + private void out(String message) { + if (!beQuiet) { + stdout.print(message + "\n"); + } + } + + private void prepareLogsPath() throws Exception { + if (logsPath == null) { + throw new Exception("Command requires the -l option"); + } + if (!logsPath.isAbsolute()) { + if (serverPath != null && serverPath.isAbsolute() && serverPath.toFile().exists()) { + logsPath = serverPath.resolve(logsPath); + } else { + throw new Exception("Logs directory must be an absolute path, or -s must be supplied"); + } + } + } + + @Override + protected void runImpl(CommandLine cli) throws Exception { + } + + public void setLogPath(Path logsPath) { + this.logsPath = logsPath; + } + + public void setServerPath(Path serverPath) { + this.serverPath = serverPath; + } + + public void setQuiet(boolean shouldPrintStdout) { + this.beQuiet = shouldPrintStdout; + } + } // end UtilsTool class } diff --git a/solr/core/src/test/org/apache/solr/util/UtilsToolTest.java b/solr/core/src/test/org/apache/solr/util/UtilsToolTest.java new file mode 100644 index 00000000000..fa39620c21f --- /dev/null +++ b/solr/core/src/test/org/apache/solr/util/UtilsToolTest.java @@ -0,0 +1,185 @@ +/* + * 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.util; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.time.Period; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.commons.cli.CommandLine; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.apache.solr.util.SolrCLI.findTool; +import static org.apache.solr.util.SolrCLI.parseCmdLine; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Unit test for SolrCLI's UtilsTool + */ +public class UtilsToolTest { + + private Path dir; + private SolrCLI.UtilsTool tool; + private List files = Arrays.asList( + "solr.log", + "solr.log.1", + "solr.log.2", + "solr.log.3", + "solr.log.9", + "solr.log.10", + "solr.log.11", + "solr_log_20160102", + "solr_log_20160304", + "solr-8983-console.log", + "solr_gc_log_20160102", + "solr_gc_log_2"); + + @Before + public void setUp() throws IOException { + dir = Files.createTempDirectory("Utils Tool Test"); + files.stream().forEach(f -> { + try { + dir.resolve(f).toFile().createNewFile(); + } catch (IOException e) { + assertTrue(false); + } + }); + } + + @After + public void tearDown() throws IOException { + org.apache.commons.io.FileUtils.deleteDirectory(dir.toFile()); + } + + @Test + public void testEmptyAndQuiet() throws Exception { + String[] args = {"utils", "-remove_old_solr_logs", "7", + "-rotate_solr_logs", "9", + "-archive_gc_logs", + "-archive_console_logs", + "-q", + "-l", dir.toString()}; + assertEquals(0, runTool(args)); + } + + @Test + public void testNonexisting() throws Exception { + String nonexisting = dir.resolve("non-existing").toString(); + String[] args = {"utils", "-remove_old_solr_logs", "7", + "-rotate_solr_logs", "9", + "-archive_gc_logs", + "-archive_console_logs", + "-l", nonexisting}; + assertEquals(0, runTool(args)); + } + + @Test + public void testRemoveOldSolrLogs() throws Exception { + String[] args = {"utils", "-remove_old_solr_logs", "1", "-l", dir.toString()}; + assertEquals(files.size(), fileCount()); + assertEquals(0, runTool(args)); + assertEquals(files.size(), fileCount()); // No logs older than 1 day + Files.setLastModifiedTime(dir.resolve("solr_log_20160102"), FileTime.from(Instant.now().minus(Period.ofDays(2)))); + assertEquals(0, runTool(args)); + assertEquals(files.size()-1, fileCount()); // One logs older than 1 day + Files.setLastModifiedTime(dir.resolve("solr_log_20160304"), FileTime.from(Instant.now().minus(Period.ofDays(3)))); + assertEquals(0, runTool(args)); + assertEquals(files.size()-2, fileCount()); // Two logs older than 1 day + } + + @Test + public void testRelativePath() throws Exception { + String[] args = {"utils", "-remove_old_solr_logs", "0", "-l", dir.getFileName().toString(), "-s", dir.getParent().toString()}; + assertEquals(files.size(), fileCount()); + assertEquals(0, runTool(args)); + assertEquals(files.size()-2, fileCount()); + } + + @Test + public void testRelativePathError() throws Exception { + String[] args = {"utils", "-remove_old_solr_logs", "0", "-l", dir.getFileName().toString()}; + try { + runTool(args); + } catch (Exception e) { + return; + } + assertTrue(false); + } + + @Test + public void testRemoveOldGcLogs() throws Exception { + String[] args = {"utils", "-archive_gc_logs", "-l", dir.toString()}; + assertEquals(files.size(), fileCount()); + assertEquals(0, runTool(args)); + assertEquals(files.size()-2, fileCount()); + assertFalse(listFiles().contains("solr_gc_log_2")); + assertTrue(Files.exists(dir.resolve("archived").resolve("solr_gc_log_2"))); + assertEquals(0, runTool(args)); + assertFalse(Files.exists(dir.resolve("archived").resolve("solr_gc_log_2"))); + } + + @Test + public void testArchiveConsoleLogs() throws Exception { + String[] args = {"utils", "-archive_console_logs", "-l", dir.toString()}; + assertEquals(files.size(), fileCount()); + assertEquals(0, runTool(args)); + assertEquals(files.size()-1, fileCount()); + assertFalse(listFiles().contains("solr-8983-console.log")); + assertTrue(Files.exists(dir.resolve("archived").resolve("solr-8983-console.log"))); + assertEquals(0, runTool(args)); + assertFalse(Files.exists(dir.resolve("archived").resolve("solr-8983-console.log"))); + } + + @Test + public void testRotateSolrLogs() throws Exception { + String[] args = {"utils", "-rotate_solr_logs", "9", "-l", dir.toString()}; + assertEquals(files.size(), fileCount()); + assertTrue(listFiles().contains("solr.log")); + assertEquals(0, runTool(args)); + assertEquals(files.size()-3, fileCount()); + assertTrue(listFiles().contains("solr.log.4")); + assertFalse(listFiles().contains("solr.log")); + assertFalse(listFiles().contains("solr.log.9")); + assertFalse(listFiles().contains("solr.log.10")); + assertFalse(listFiles().contains("solr.log.11")); + } + + private List listFiles() throws IOException { + return Files.find(dir, 1, (p, a) -> a.isRegularFile()).map(p -> p.getFileName().toString()).collect(Collectors.toList()); + } + + private long fileCount() throws IOException { + return listFiles().size(); + } + + private int runTool(String[] args) throws Exception { + SolrCLI.Tool tool = findTool(args); + CommandLine cli = parseCmdLine(args, tool.getOptions()); + return tool.runTool(cli); + } +} \ No newline at end of file