/* * * * RHQ Management Platform * * Copyright (C) 2005-2013 Red Hat, Inc. * * All rights reserved. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License, version 2, as * * published by the Free Software Foundation, and/or the GNU Lesser * * General Public License, version 2.1, also as published by the Free * * Software Foundation. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License and the GNU Lesser General Public License * * for more details. * * * * You should have received a copy of the GNU General Public License * * and the GNU Lesser General Public License along with this program; * * if not, write to the Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ package org.rhq.server.control; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.apache.commons.configuration.ConfigurationException; import org.apache.commons.configuration.PropertiesConfiguration; import org.apache.commons.exec.DefaultExecutor; import org.apache.commons.exec.ExecuteException; import org.apache.commons.exec.Executor; import org.apache.commons.exec.PumpStreamHandler; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.rhq.core.util.stream.StreamUtil; /** * @author John Sanda */ public abstract class ControlCommand { public static final String SERVER_OPTION = "server"; public static final String STORAGE_OPTION = "storage"; public static final String AGENT_OPTION = "agent"; public static final String HELP_OPTION_1 = "--help"; public static final String HELP_OPTION_2 = "-h"; public static final String RHQ_AGENT_BASEDIR_PROP = "rhq.agent.basedir"; protected static final String STORAGE_BASEDIR_NAME = "rhq-storage"; protected static final String AGENT_BASEDIR_NAME = "rhq-agent"; protected static final String RHQ_SERVER_PIDFILE_DIR_ENV_VARIABLE = "RHQ_SERVER_PIDFILE_DIR"; protected static final String RHQ_AGENT_PIDFILE_DIR_ENV_VARIABLE = "RHQ_AGENT_PIDFILE_DIR"; private final File defaultStorageBasedir; private final File defaultAgentBasedir; protected final Log log = LogFactory.getLog(getClass().getName()); private File basedir; private File binDir; // where the internal startup scripts are private PropertiesConfiguration rhqctlConfig = null; // the (optional) settings found in rhqctl.properties private static final int WAIT_FOR_PROCESS_TO_STOP_TIMEOUT_SECS; static { int propInt; try { propInt = Integer.parseInt(System.getProperty("rhqctl.wait-for-process-to-stop-timeout-secs", "30")); } catch (Exception e) { propInt = 30; } WAIT_FOR_PROCESS_TO_STOP_TIMEOUT_SECS = propInt; } private ArrayList<Runnable> undoTasks = new ArrayList<Runnable>(); protected void undo() { // perform the undo on the snapshot of undoTasks, because of possible ConcurrentModificationException List<Runnable> undoTasksCopy = new ArrayList<Runnable>(undoTasks); Collections.reverse(undoTasksCopy); // do the undo tasks in the reverse order in which they were added to the list for (Runnable undoTask : undoTasksCopy) { try { undoTask.run(); } catch (Throwable t) { log.error("Failed to invoke undo task [" + undoTask + "], will keep going but system may be in an indeterminate state"); } } return; } protected void addUndoTask(UndoTask r) { undoTasks.add(r); } protected abstract class UndoTask implements Runnable { private final String message; public UndoTask() { this(null); } public UndoTask(String msg) { this.message = msg; } @Override public void run() { if (this.message != null) { log.warn("UNDO: " + this.message); } try { performUndoWork(); } catch (Exception e) { throw new RuntimeException("Undo task failed: " + ((this.message == null) ? "" : this.message), e); } } protected abstract void performUndoWork() throws Exception; } public ControlCommand() { basedir = new File(System.getProperty("rhq.server.basedir")); binDir = new File(basedir, "bin/internal"); File rhqctlPropertiesFile = getRhqCtlProperties(); if (rhqctlPropertiesFile != null) { try { rhqctlConfig = new PropertiesConfiguration(rhqctlPropertiesFile); } catch (ConfigurationException e) { throw new RHQControlException("Failed to load configuration", e); } } defaultStorageBasedir = new File(getBaseDir(), STORAGE_BASEDIR_NAME); defaultAgentBasedir = new File(getBaseDir().getParent(), AGENT_BASEDIR_NAME); } public abstract String getName(); public abstract String getDescription(); public abstract Options getOptions(); protected abstract int exec(CommandLine commandLine); public int exec(String[] args) { Options options = getOptions(); int rValue = RHQControl.EXIT_CODE_OK; try { CommandLineParser parser = new RHQPosixParser(false); CommandLine cmdLine = parser.parse(options, args, true); if (!cmdLine.getArgList().isEmpty()) { // there were some unrecognized args System.out.println("Unrecognized arguments: " + cmdLine.getArgList()); printUsage(); return RHQControl.EXIT_CODE_INVALID_ARGUMENT; } rValue = exec(cmdLine); if (rhqctlConfig != null) { rhqctlConfig.save(); } } catch (ParseException e) { printUsage(); rValue = RHQControl.EXIT_CODE_INVALID_ARGUMENT; } catch (ConfigurationException e) { throw new RHQControlException("Failed to update " + getRhqCtlProperties(), e); } return rValue; } public void printUsage() { Options options = getOptions(); HelpFormatter helpFormatter = new HelpFormatter(); String header = "\n" + getDescription() + "\n\n"; String syntax; if (options.getOptions().isEmpty()) { syntax = "rhqctl " + getName(); } else { syntax = "rhqctl " + getName() + " [options]"; } helpFormatter.setNewLine("\n"); helpFormatter.printHelp(syntax, header, options, null); // we can't pass this as the last argument to printHelp above // because it throws an exception if the string is too long. String readmeContent = getReadmeContent(); if (readmeContent != null) { System.out.println(readmeContent); } } /** * Subclasses can override this and return a non-null name of its readme file. * If non-null is returned, the content of that file will be dumped at the bottom of the * --help output. * @return readme help file name, or null if the command doesn't have one */ abstract protected String getReadmeFilename(); protected String getReadmeContent() { // see if the command has a readme file whose content we will print; if not, just abort String readmeFilename = getReadmeFilename(); if (readmeFilename == null) { return null; } File readmeFile = new File(getBaseDir(), readmeFilename); if (!readmeFile.canRead()) { return null; } // dump the readme try { FileReader fileReader = new FileReader(readmeFile); String readmeContent = StreamUtil.slurp(fileReader); return readmeContent; } catch (Exception e) { // just quietly abort, don't be noisy about this, though this should never happen return null; } } protected List<Integer> toIntList(String s) { String[] args = s.split(","); List<Integer> list = new ArrayList<Integer>(args.length); for (String arg : args) { list.add(Integer.parseInt(arg)); } return list; } protected File getBaseDir() { return this.basedir; } protected File getBinDir() { return this.binDir; } protected File getLogDir() { return new File(getBaseDir(), "logs"); } protected File getStorageBasedir() { return defaultStorageBasedir; // the storage location is always in our expected default location, not configurable } protected File getAgentBasedir() { File retval; String agentBasedirProperty = getProperty(RHQ_AGENT_BASEDIR_PROP, null); if (agentBasedirProperty == null) { retval = defaultAgentBasedir; // we want to return the exact defaultAgentBasedir object here to ensure equals() works later } else { retval = new File(agentBasedirProperty); } return retval; } protected void setAgentBasedir(File agentBasedir) { // We only want to create this in rhqctl.properties if it is different than the default location. // This allows us to avoid creating an empty/unused rhqctl.properties - we only want that file // if we actually need it to override our defaults. if (agentBasedir != null && !agentBasedir.equals(defaultAgentBasedir)) { putProperty(RHQ_AGENT_BASEDIR_PROP, agentBasedir.getAbsolutePath()); } } protected boolean isServerInstalled() { return isServerInstalled(getBaseDir()); } protected File getServerInstalledMarkerFile(File baseDir) { return new File(baseDir, "jbossas/standalone/data/rhq.installed"); } protected File getServerPropertiesFile() { return new File(getBaseDir(), "bin/rhq-server.properties"); } protected boolean isServerInstalled(File baseDir) { File markerFile = getServerInstalledMarkerFile(baseDir); return markerFile.exists(); } protected boolean isAgentInstalled() { return getAgentBasedir().exists(); } protected boolean isStorageInstalled() { return getStorageBasedir().exists(); } protected File getStoragePidFile() { File storageBasedir = getStorageBasedir(); File storageBinDir = new File(storageBasedir, "bin"); File pidFile = new File(storageBinDir, "cassandra.pid"); return pidFile; } protected String getStoragePid() throws IOException { File pidFile = getStoragePidFile(); if (pidFile.exists()) { return StreamUtil.slurp(new FileReader(pidFile)); } return null; } /** * Returns a property from the rhqctl.properties file. If that optional file doesn't * exist, the default value is returned. If the file exists but the key isn't found * in there, the default value is returned. * @param key the property name to find in rhqctl.properties * @param defaultValue the value to return if the property doesn't exist * @return the value of the property or its default value if property does not exist */ private String getProperty(String key, String defaultValue) { return (rhqctlConfig != null) ? rhqctlConfig.getString(key, defaultValue) : defaultValue; } /** * Sets a property to rhqctl.properties. Note that calling this will eventually cause * rhqctl.properties file to be created later. * @param key * @param value */ private void putProperty(String key, String value) { if (rhqctlConfig == null) { rhqctlConfig = new PropertiesConfiguration(); rhqctlConfig.setPath(getRhqCtlPropertiesPath()); } rhqctlConfig.setProperty(key, value); } /** * Returns the file of the rhqctl.properties file if it exists. Since this is * an optional file, this can return null to indicate it doesn't yet exist. * * @return rhqctl.properties file if it exists; null if it doesn't exist */ protected File getRhqCtlProperties() { String filename = getRhqCtlPropertiesPath(); File file = new File(filename); if (!file.isFile()) { file = null; } return file; } /** * Returns the path to the optional rhqctl.properties file. This will always return non-null, * even if the file actually doesn't exist yet. * @return the path to where the rhqctl.properties file is or could be if one existed. */ protected String getRhqCtlPropertiesPath() { String sysprop = System.getProperty("rhqctl.properties-file"); if (sysprop == null) { throw new RuntimeException("The required system property [rhqctl.properties-file] is not defined."); } return sysprop; } protected org.apache.commons.exec.CommandLine getCommandLine(String scriptName, String... args) { return getCommandLine(true, scriptName, args); } protected org.apache.commons.exec.CommandLine getCommandLine(boolean addShExt, String scriptName, String... args) { org.apache.commons.exec.CommandLine result; if (isWindows()) { result = new org.apache.commons.exec.CommandLine("cmd.exe"); result.addArgument("/C"); result.addArgument(scriptName.replace('/', '\\') + ".bat"); } else { result = new org.apache.commons.exec.CommandLine("./" + (addShExt ? scriptName + ".sh" : scriptName)); } for (String arg : args) { result.addArgument(arg); } return result; } /** * For console commands on Windows only */ protected org.apache.commons.exec.CommandLine getConsoleCommandLine(String scriptName, String... args) { org.apache.commons.exec.CommandLine result; result = new org.apache.commons.exec.CommandLine("cmd.exe"); result.addArgument("/C"); result.addArgument("start"); result.addArgument("/I"); result.addArgument(scriptName.replace('/', '\\') + ".bat"); for (String arg : args) { result.addArgument(arg); } return result; } protected String getScript(String scriptName) { if (isWindows()) { return scriptName.replace('/', '\\') + ".bat"; } return "./" + scriptName + ".sh"; } protected boolean isWindows() { String operatingSystem = System.getProperty("os.name").toLowerCase(Locale.US); return operatingSystem.contains("windows"); } protected boolean isPortInUse(String host, int port) { boolean inUse; try { Socket testSocket = new Socket(host, port); try { testSocket.close(); } catch (Exception ignore) { } inUse = true; } catch (Exception expected) { inUse = false; } return inUse; } protected void waitForProcessToStop(String pid) throws Exception { if (isWindows() || pid == null) { // For the moment we have no better way to just wait some time Thread.sleep(WAIT_FOR_PROCESS_TO_STOP_TIMEOUT_SECS * 1000L); } else { if (!waitForProcess(pid, WAIT_FOR_PROCESS_TO_STOP_TIMEOUT_SECS * 2, 10)) { log.warn("Process [" + pid + "] did not finish yet, retrying with SIGKILL signal"); killPid(pid, "KILL"); if (!waitForProcess(pid, WAIT_FOR_PROCESS_TO_STOP_TIMEOUT_SECS, 5)) { throw new RHQControlException("Process [" + pid + "] did not finish yet. Terminate it manually and retry."); } } } } /** * waits until process with given pid dies * @param pid process PID * @param maxTries number of checks to be performed within timeout * @param timeout waiting timeout in seconds * @return true if process does not run anymore, false otherwise * @throws Exception */ private boolean waitForProcess(String pid, final int timeout, final int maxTries) throws Exception { int tries = 1; while (tries <= maxTries) { log.debug("Waiting for pid [" + pid + "] to stop. Try #" + tries); if (!isUnixPidRunning(pid)) { return true; } Thread.sleep((timeout / maxTries) * 1000L); tries++; } return false; } protected void killPid(String pid, String signal) throws IOException { Executor executor = new DefaultExecutor(); executor.setWorkingDirectory(getBinDir()); PumpStreamHandler streamHandler = new PumpStreamHandler(createNullOutputStream(), createNullOutputStream()); executor.setStreamHandler(streamHandler); org.apache.commons.exec.CommandLine commandLine; commandLine = new org.apache.commons.exec.CommandLine("kill"); if (signal != null) { commandLine.addArgument("-" + signal); } commandLine.addArgument(pid); executor.execute(commandLine); } protected void killPid(String pid) throws IOException { killPid(pid, null); } protected boolean isUnixPidRunning(String pid) { try { Integer numericPid = Integer.valueOf(pid); } catch(NumberFormatException e) { // PID wasn't numeric, it can't be correct return false; } Executor executor = new DefaultExecutor(); executor.setWorkingDirectory(getBinDir()); PumpStreamHandler streamHandler = new PumpStreamHandler(createNullOutputStream(), createNullOutputStream()); executor.setStreamHandler(streamHandler); org.apache.commons.exec.CommandLine commandLine; commandLine = new org.apache.commons.exec.CommandLine("kill").addArgument("-0").addArgument(pid); boolean isRunning = true; // assume it is running try { int code = executor.execute(commandLine); if (code != 0) { isRunning = false; } } catch (ExecuteException ee) { log.debug("kill -0 for pid [" + pid + "] threw exception with exit value [" + ee.getExitValue() + "]"); if (ee.getExitValue() == 1) { // return code 1 means process does not exist isRunning = false; } } catch (IOException e) { log.error("Checking for running process failed. Will assume it is running. Error: " + e.getMessage()); } log.debug("unix pid [" + pid + "] " + ((isRunning) ? "is" : "is NOT") + " running"); return isRunning; } protected boolean isStorageRunning() throws IOException { String pid = getStoragePid(); if (pid == null) { return false; } else if (pid != null && !isUnixPidRunning(pid)) { // There is a phantom pidfile File pidFile = getStoragePidFile(); if (!pidFile.delete()) { throw new RHQControlException("Could not delete storage pidfile " + pidFile.getAbsolutePath()); } return false; } else { return true; } } /** * Call this method to get an output stream that throws away all data. Useful * when executing commands which you don't care about seeing its output on stdout. * Example usage: * executor.setStreamHandler(new PumpStreamHandler(createNullOutputStream(), createNullOutputStream())); * @return a null output stream */ protected OutputStream createNullOutputStream() { return new NullOutputStream(); } private class NullOutputStream extends OutputStream { @Override public void write(byte[] b, int off, int len) { } @Override public void write(int b) { } @Override public void write(byte[] b) throws IOException { } } /** * reads rhq-server.properties file and outputs warning in case there are trailing spaces */ protected void validateServerPropertiesFile() throws IOException { InputStreamReader isr = new InputStreamReader(new FileInputStream(getServerPropertiesFile())); BufferedReader in = new BufferedReader(isr); for (String line = in.readLine(); line != null; line = in.readLine()) { if (line.matches("(?!^[ \t]*#).*[ \t]+$")) { log.warn("Line \"" + line + "\" contains trailing white-spaces, fix " + getServerPropertiesFile() + " if you encounter issues"); } } in.close(); } }