/* * Jopr Management Platform * Copyright (C) 2005-2008 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.jboss.on.plugins.tomcat; import java.io.File; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.Map; import java.util.StringTokenizer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.mc4j.ems.connection.EmsConnection; import org.mc4j.ems.connection.bean.EmsBean; import org.mc4j.ems.connection.bean.operation.EmsOperation; import org.jboss.on.plugins.tomcat.TomcatServerComponent.ControlMethod; import org.jboss.on.plugins.tomcat.TomcatServerComponent.SupportedOperations; import org.rhq.core.domain.configuration.Configuration; import org.rhq.core.domain.configuration.Property; import org.rhq.core.domain.configuration.PropertyList; import org.rhq.core.domain.configuration.PropertyMap; import org.rhq.core.domain.configuration.PropertySimple; import org.rhq.core.domain.measurement.AvailabilityType; import org.rhq.core.pluginapi.operation.OperationResult; import org.rhq.core.pluginapi.util.ProcessExecutionUtility; import org.rhq.core.system.OperatingSystemType; import org.rhq.core.system.ProcessExecution; import org.rhq.core.system.ProcessExecutionResults; import org.rhq.core.system.SystemInfo; /** * Handles performing operations on a Tomcat Server * * @author Jay Shaughnessy * @author Ian Springer * @author Jason Dobies * @author Lukas Krejci */ public class TomcatServerOperationsDelegate { public static final String SHUTDOWN_SCRIPT_ENVIRONMENT_PROPERTY = "shutdownScriptEnvironment"; public static final String START_SCRIPT_ENVIRONMENT_PROPERTY = "startScriptEnvironment"; private static final String SERVER_MBEAN_NAME = "Catalina:type=Server"; /** max amount of time to wait for server to show as unavailable after executing stop - in milliseconds */ private static long STOP_WAIT_MAX = 1000L * 150; // 2.5 minutes /** amount of time to wait between availability checks when performing a stop - in milliseconds */ private static final long STOP_WAIT_INTERVAL = 1000L * 10; // 10 seconds /** amount of time to wait for stop to complete after the loop that checks for DOWN availability terminates - * in milliseconds */ private static final long STOP_WAIT_FINAL = 1000L * 30; // 30 seconds /** max amount of time to wait for start to complete - in milliseconds */ private static long START_WAIT_MAX = 1000L * 300; // 5 minutes /** amount of time to wait between availability checks when performing a start - in milliseconds */ private static final long START_WAIT_INTERVAL = 1000L * 10; // 10 seconds private final Log log = LogFactory.getLog(this.getClass()); private static final String SEPARATOR = "\n-----------------------\n"; // Attributes -------------------------------------------- /** * Server component against which the operations are being performed. */ private TomcatServerComponent serverComponent; /** * Passed in from the resource context for making process calls. */ private SystemInfo systemInfo; // Constructors -------------------------------------------- public TomcatServerOperationsDelegate(TomcatServerComponent serverComponent, SystemInfo systemInfo) { this.serverComponent = serverComponent; this.systemInfo = systemInfo; } // Public -------------------------------------------- /** * Performs the specified operation. The result of the operation will be indicated in the return. If there is an * error, an <code>RuntimeException</code> will be thrown. * * @param operation the operation to perform * @param parameters parameters to the operation call * * @return if successful, the result object will contain a success message * * @throws RuntimeException if any errors occur while trying to perform the operation */ public OperationResult invoke(SupportedOperations operation, Configuration parameters) throws InterruptedException { String message = null; switch (operation) { case RESTART: { message = restart(parameters); break; } case SHUTDOWN: { message = shutdown(parameters); break; } case START: { message = start(parameters); break; } case STORECONFIG: { message = storeConfig(); break; } } OperationResult result = new OperationResult(message); return result; } // Private -------------------------------------------- private String start(Configuration parameters) throws InterruptedException { PropertyList env = parameters.getList(START_SCRIPT_ENVIRONMENT_PROPERTY); return start(env); } /** * Starts the underlying server. * * @return success message if no errors are encountered * @throws InterruptedException if the plugin container stops this operation while its executing */ private String start(PropertyList environment) throws InterruptedException { Configuration pluginConfiguration = this.serverComponent.getPluginConfiguration(); String controlMethodName = pluginConfiguration.getSimpleValue( TomcatServerComponent.PLUGIN_CONFIG_CONTROL_METHOD, ControlMethod.SCRIPT.name()); ControlMethod controlMethod = ControlMethod.valueOf(controlMethodName); ProcessExecution processExecution = (controlMethod == ControlMethod.SCRIPT) ? getScriptStart(pluginConfiguration) : getRpmStart(pluginConfiguration); applyEnvironmentVars(environment, processExecution); long start = System.currentTimeMillis(); if (log.isDebugEnabled()) { log.debug("About to execute the following process: [" + processExecution + "]"); } ProcessExecutionResults results = this.systemInfo.executeProcess(processExecution); logExecutionResults(results); Throwable error = results.getError(); Integer exitCode = results.getExitCode(); AvailabilityType avail; if (startScriptFailed(controlMethod, error, exitCode, isWindows())) { String output = results.getCapturedOutput(); String message = "Script returned error or non-zero exit code while starting the Tomcat instance - exitCode=[" + ((exitCode != null) ? exitCode : "UNKNOWN") + "], output=[" + output + "]."; if (error == null) { log.error(message); } else { log.error(message, error); } avail = this.serverComponent.getAvailability(); } else { avail = waitForServerToStart(start); } // If, after the loop, the Server is still down, consider the start to be a failure. if (avail == AvailabilityType.DOWN) { throw new RuntimeException("Server failed to start: " + results.getCapturedOutput()); } else { return "Server has been started."; } } private static boolean startScriptFailed(ControlMethod controlMethod, Throwable error, Integer exitCode, boolean isWindows) { if (error != null || exitCode == null) { return true; } if (controlMethod == ControlMethod.SCRIPT && isWindows) { // Believe it or not, an exit code of 1 from startup.bat does not indicate an error. return exitCode != 0 && exitCode != 1; } else { return exitCode != 0; } } private ProcessExecution getScriptStart(Configuration pluginConfiguration) { File startScriptFile = this.serverComponent.getStartScriptPath(); validateScriptFile(startScriptFile, TomcatServerComponent.PLUGIN_CONFIG_START_SCRIPT); String prefix = pluginConfiguration.getSimple(TomcatServerComponent.PLUGIN_CONFIG_SCRIPT_PREFIX) .getStringValue(); ProcessExecution processExecution; // prefix is either null or contains ONLY whitespace characters if (prefix == null || prefix.replaceAll("\\s", "").equals("")) { processExecution = ProcessExecutionUtility.createProcessExecution(startScriptFile); } else { // The process execution should be tied to the process represented as the prefix. If there are any other // tokens in the prefix, consider them arguments to the prefix process. StringTokenizer prefixTokenizer = new StringTokenizer(prefix); String processName = prefixTokenizer.nextToken(); File prefixProcess = new File(processName); processExecution = ProcessExecutionUtility.createProcessExecution(prefixProcess); while (prefixTokenizer.hasMoreTokens()) { String prefixArgument = prefixTokenizer.nextToken(); processExecution.getArguments().add(prefixArgument); } // Assemble the AS start script and its prefixes as one argument to the prefix String startScriptArgument = startScriptFile.getAbsolutePath(); processExecution.getArguments().add(startScriptArgument); } initScriptProcessExecution(processExecution, startScriptFile); return processExecution; } private ProcessExecution getRpmStart(Configuration pluginConfiguration) { ProcessExecution processExecution; String rpm = getTomcatServiceNum(); if (isWindows()) { processExecution = new ProcessExecution("net"); // disable the executable existence check because it is a command on the supplied PATH processExecution.setCheckExecutableExists(false); processExecution.setArguments(new ArrayList<String>()); processExecution.getArguments().add("start"); processExecution.getArguments().add(rpm); } else { processExecution = new ProcessExecution("service"); // disable the executable existence check because it is a command on the supplied PATH processExecution.setCheckExecutableExists(false); processExecution.setArguments(new ArrayList<String>()); processExecution.getArguments().add(rpm); processExecution.getArguments().add("start"); } Map<String, String> envVars = new LinkedHashMap<String, String>(System.getenv()); processExecution.setEnvironmentVariables(envVars); initProcessExecution(processExecution); return processExecution; } private String shutdown(Configuration parameters) throws InterruptedException { PropertyList env = parameters.getList(SHUTDOWN_SCRIPT_ENVIRONMENT_PROPERTY); return shutdown(env); } private String shutdown(PropertyList environment) throws InterruptedException { String result = doShutdown(environment); AvailabilityType avail = waitForServerToShutdown(); if (avail == AvailabilityType.UP) { throw new RuntimeException("Server failed to shutdown"); } else { return result; } } private boolean isWindows() { return this.systemInfo.getOperatingSystemType() == OperatingSystemType.WINDOWS; } /** * Shuts down the AS server using a shutdown script. * * @return success message if no errors are encountered */ private String doShutdown(PropertyList environment) { Configuration pluginConfiguration = this.serverComponent.getPluginConfiguration(); // NOTE: In TomcatDiscoveryComponent.java we set TomcatServerComponent.PLUGIN_CONFIG_SHUTDOWN_SCRIPT (amongst others). String controlMethod = pluginConfiguration.getSimpleValue(TomcatServerComponent.PLUGIN_CONFIG_CONTROL_METHOD, ControlMethod.SCRIPT.name()); ProcessExecution processExecution = (ControlMethod.SCRIPT.name().equals(controlMethod)) ? getScriptShutdown(pluginConfiguration) : getRpmShutdown(); applyEnvironmentVars(environment, processExecution); if (log.isDebugEnabled()) { log.debug("About to execute the following process: [" + processExecution + "]"); } ProcessExecutionResults results = this.systemInfo.executeProcess(processExecution); logExecutionResults(results); Throwable error = results.getError(); Integer exitCode = results.getExitCode(); if ((null != error) || ((null != exitCode) && (0 != exitCode))) { String message = "Script returned error or non-zero exit code while shutting down the Tomcat instance. Exit code [" + exitCode + "]"; if (null == error) { throw new RuntimeException(message); } else { throw new RuntimeException(message, error); } } return "Server has been shut down."; } private ProcessExecution getScriptShutdown(Configuration pluginConfiguration) { File shutdownScriptFile = this.serverComponent.getShutdownScriptPath(); validateScriptFile(shutdownScriptFile, TomcatServerComponent.PLUGIN_CONFIG_SHUTDOWN_SCRIPT); String prefix = pluginConfiguration.getSimple(TomcatServerComponent.PLUGIN_CONFIG_SCRIPT_PREFIX) .getStringValue(); ProcessExecution processExecution = ProcessExecutionUtility.createProcessExecution(prefix, shutdownScriptFile); initScriptProcessExecution(processExecution, shutdownScriptFile); return processExecution; } private String getTomcatServiceNum() { String catalinaHome = this.serverComponent.getCatalinaHome().getPath(); String serviceName = this.serverComponent.getServiceName(); if (serviceName != null) return serviceName; String rpm = TomcatDiscoveryComponent.EWS_TOMCAT_8; if (TomcatDiscoveryComponent.isTomcat7(catalinaHome)) rpm = TomcatDiscoveryComponent.EWS_TOMCAT_7; if (TomcatDiscoveryComponent.isTomcat6(catalinaHome)) rpm = TomcatDiscoveryComponent.EWS_TOMCAT_6; if (TomcatDiscoveryComponent.isTomcat5(catalinaHome)) rpm = TomcatDiscoveryComponent.EWS_TOMCAT_5; return rpm; } private ProcessExecution getRpmShutdown() { ProcessExecution processExecution; String rpm = getTomcatServiceNum(); if (isWindows()) { processExecution = new ProcessExecution("net"); // disable the executable existence check because it is a command on the supplied PATH processExecution.setCheckExecutableExists(false); processExecution.setArguments(new ArrayList<String>()); processExecution.getArguments().add("stop"); processExecution.getArguments().add(rpm); } else { processExecution = new ProcessExecution("service"); // disable the executable existence check because it is a command on the supplied PATH processExecution.setCheckExecutableExists(false); processExecution.setArguments(new ArrayList<String>()); processExecution.getArguments().add(rpm); processExecution.getArguments().add("stop"); } Map<String, String> envVars = new LinkedHashMap<String, String>(System.getenv()); log.info("Operation Envs: " + envVars); processExecution.setEnvironmentVariables(envVars); initProcessExecution(processExecution); return processExecution; } static public void setProcessExecutionEnvironment(ProcessExecution processExecution, String catalinaHome, String catalinaBase) { String jreHomeDir = System.getProperty("java.home"); if (null == jreHomeDir) { throw new IllegalStateException( "The JAVA_HOME or JAVA_JRE environment variable must be set in order to run the Tomcat scripts."); } Map<String, String> processExecutionEnvironmentVariables = processExecution.getEnvironmentVariables(); if (null == processExecutionEnvironmentVariables) { processExecutionEnvironmentVariables = new LinkedHashMap<String, String>(); processExecution.setEnvironmentVariables(processExecutionEnvironmentVariables); } // It is important to realize that the processExecutionEnvironmentVariables may have been inheriting the // environment of the RHQ Agent. The RHQ Agent allows for JAVA_HOME to be set to a JRE and does not // use JRE_HOME. Tomcat does not allow this, and favors the use of JRE_HOME. So, unset JAVA_HOME and // reset it as needed. Always set JRE_HOME. processExecutionEnvironmentVariables.remove("JAVA_HOME"); processExecutionEnvironmentVariables.put("JRE_HOME", new File(jreHomeDir).getPath()); processExecutionEnvironmentVariables.put("CATALINA_HOME", catalinaHome); processExecutionEnvironmentVariables.put("CATALINA_BASE", catalinaBase); processExecutionEnvironmentVariables.put("CATALINA_TMPDIR", catalinaBase + File.separator + "temp"); // Tomcat, starting with 5.5, requires only a JRE to run. But, if TC is running in debug mode it // requires a JDK. We always set JRE_HOME above but, if possible, set JAVA_HOME as well if // in fact it looks like we have a JDK at our disposal. if (jreHomeDir.endsWith("jre")) { File jdkHomeDir = new File(jreHomeDir.substring(0, jreHomeDir.length() - 3)); // one more check, look for a bin dir if (new File(jdkHomeDir, "bin").isDirectory()) { processExecutionEnvironmentVariables.put("JAVA_HOME", jdkHomeDir.getPath()); } } } private void initScriptProcessExecution(ProcessExecution processExecution, File scriptFile) { // For the script to work the current working dir must be set to the script's parent dir processExecution.setWorkingDirectory(scriptFile.getParent()); // Set necessary environment variables setProcessExecutionEnvironment(processExecution, this.serverComponent.getCatalinaHome().getPath(), this.serverComponent.getCatalinaBase().getPath()); initProcessExecution(processExecution); } private void initProcessExecution(ProcessExecution processExecution) { processExecution.setCaptureOutput(true); processExecution.setWaitForCompletion(120000L); // 120 seconds - that should be safe? // TODO: make this configurable processExecution.setKillOnTimeout(false); } private void logExecutionResults(ProcessExecutionResults results) { if (log.isDebugEnabled()) { log.debug("Exit code from process execution: " + results.getExitCode()); log.debug("Output from process execution: " + SEPARATOR + results.getCapturedOutput() + SEPARATOR); } } private String restart(Configuration parameters) { StringBuffer result = new StringBuffer(); boolean problem = false; try { shutdown(parameters); } catch (Exception e) { problem = true; result.append("Shutdown may have failed: "); result.append(e); result.append(", "); } finally { try { // Wait for server to show as unavailable, up to max wait time. AvailabilityType avail = waitForServerToShutdown(); if (avail == AvailabilityType.UP) { problem = true; result.append("Shutdown may have failed (server appears to still be running), "); } // Perform the restart. start(parameters); } catch (Exception e) { problem = true; result.append("Startup may have failed: "); result.append(e); result.append(", "); } } if (problem) { result.append("Restart may have failed."); } else { result.append("Server has been restarted."); } return result.toString(); } private void validateScriptFile(File scriptFile, String scriptPropertyName) { if (!scriptFile.exists()) { throw new RuntimeException("Script (" + scriptFile + ") specified via '" + scriptPropertyName + "' connection property does not exist."); } if (scriptFile.isDirectory()) { throw new RuntimeException("Script (" + scriptFile + ") specified via '" + scriptPropertyName + "' connection property is a directory, not a file."); } } private AvailabilityType waitForServerToStart(long start) throws InterruptedException { AvailabilityType avail; //detect whether startWaitMax property has been set. Configuration pluginConfig = serverComponent.getResourceContext().getPluginConfiguration(); PropertySimple property = pluginConfig.getSimple(TomcatServerComponent.START_WAIT_MAX_PROP); //if set and valid, update startWaitMax value if ((property != null) && (property.getIntegerValue() != null)) { int newValue = property.getIntegerValue(); if (newValue >= 1) { START_WAIT_MAX = 1000L * 60 * newValue; } } while (((avail = this.serverComponent.getAvailability()) == AvailabilityType.DOWN) && (System.currentTimeMillis() < (start + START_WAIT_MAX))) { Thread.sleep(START_WAIT_INTERVAL); } return avail; } private AvailabilityType waitForServerToShutdown() throws InterruptedException { //detect whether stopWaitMax property has been set. Configuration pluginConfig = serverComponent.getResourceContext().getPluginConfiguration(); PropertySimple property = pluginConfig.getSimple(TomcatServerComponent.STOP_WAIT_MAX_PROP); //if set and valid, update startWaitMax value if ((property != null) && (property.getIntegerValue() != null)) { int newValue = property.getIntegerValue(); if (newValue >= 1) { STOP_WAIT_MAX = 1000L * 60 * newValue; } } for (long wait = 0L; (wait < STOP_WAIT_MAX) && (AvailabilityType.UP == this.serverComponent.getAvailability()); wait += STOP_WAIT_INTERVAL) { Thread.sleep(STOP_WAIT_INTERVAL); } // After the server shows unavailable, wait a little longer to hopefully ensure shutdown is complete. Thread.sleep(STOP_WAIT_FINAL); return this.serverComponent.getAvailability(); } private String storeConfig() { EmsConnection connection = this.serverComponent.getEmsConnection(); if (connection == null) { throw new RuntimeException("Can not connect to the server"); } EmsBean bean = connection.getBean(SERVER_MBEAN_NAME); EmsOperation operation = bean.getOperation("storeConfig"); operation.invoke(new Object[0]); return ("Tomcat configuration updated."); } private static void applyEnvironmentVars(PropertyList environment, ProcessExecution processExecution) { if (environment != null) { Map<String, String> environmentVariables = processExecution.getEnvironmentVariables(); for (Property prop : environment.getList()) { PropertyMap var = (PropertyMap) prop; environmentVariables.put(var.getSimpleValue("name", null), var.getSimpleValue("value", null)); } processExecution.setEnvironmentVariables(environmentVariables); } } }