/*
* 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.rhq.plugins.jbossas;
import java.io.File;
import java.util.List;
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.mc4j.ems.connection.bean.parameter.EmsParameter;
import org.rhq.core.domain.configuration.Configuration;
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.ProcessExecution;
import org.rhq.core.system.ProcessExecutionResults;
import org.rhq.core.system.SystemInfo;
/**
* Handles performing operations on a JBossAS instance.
*
* @author Ian Springer
* @author Jason Dobies
* @author Jay Shaughnessy
*/
public class JBossASServerOperationsDelegate {
/** 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(JBossASServerOperationsDelegate.class);
private static final String SEPARATOR = "\n-----------------------\n";
// Attributes --------------------------------------------
/**
* Server component against which the operations are being performed.
*/
private JBossASServerComponent serverComponent;
/**
* Passed in from the resource context for making process calls.
*/
private SystemInfo systemInfo;
// Constructors --------------------------------------------
public JBossASServerOperationsDelegate(JBossASServerComponent 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(JBossASServerSupportedOperations operation, Configuration parameters)
throws InterruptedException {
String message = null;
switch (operation) {
case RESTART: {
message = restart();
break;
}
case SHUTDOWN: {
message = shutdown();
break;
}
case START: {
message = start();
break;
}
}
OperationResult result = new OperationResult(message);
return result;
}
// Private --------------------------------------------
/**
* Starts the underlying AS server.
*
* @return success message if no errors are encountered
* @throws InterruptedException if the plugin container stops this operation while its executing
*/
private String start() throws InterruptedException {
Configuration pluginConfiguration = this.serverComponent.getPluginConfiguration();
File startScriptFile = this.serverComponent.getStartScriptPath();
validateScriptFile(startScriptFile, JBossASServerComponent.START_SCRIPT_CONFIG_PROP);
String prefix = pluginConfiguration.getSimple(JBossASServerComponent.SCRIPT_PREFIX_CONFIG_PROP)
.getStringValue();
String configName = this.serverComponent.getConfigurationSet();
String bindingAddress = pluginConfiguration.getSimple(JBossASServerComponent.BINDING_ADDRESS_CONFIG_PROP)
.getStringValue();
String configArgument = "-c" + configName;
String bindingAddressArgument = null;
if (bindingAddress != null)
bindingAddressArgument = "-b" + bindingAddress;
ProcessExecution processExecution;
// prefix is either null or contains ONLY whitespace characters
if (prefix == null || prefix.replaceAll("\\s", "").equals("")) {
processExecution = ProcessExecutionUtility.createProcessExecution(startScriptFile);
processExecution.getArguments().add("-c");
processExecution.getArguments().add(configName);
if (bindingAddressArgument != null) {
processExecution.getArguments().add("-b");
processExecution.getArguments().add(bindingAddress);
}
} 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();
startScriptArgument += " " + configArgument;
if (bindingAddressArgument != null) {
startScriptArgument += " " + bindingAddressArgument;
}
processExecution.getArguments().add(startScriptArgument);
}
initProcessExecution(processExecution, startScriptFile);
long start = System.currentTimeMillis();
if (log.isDebugEnabled()) {
log.debug("About to execute the following process: [" + processExecution + "]");
}
ProcessExecutionResults results = this.systemInfo.executeProcess(processExecution);
logExecutionResults(results);
AvailabilityType avail;
if (results.getError() == null) {
avail = waitForServerToStart(start);
} else {
log.error("Error from process execution while starting the AS instance. Exit code ["
+ results.getExitCode() + "]", results.getError());
avail = this.serverComponent.getAvailability();
}
// 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 void initProcessExecution(ProcessExecution processExecution, File scriptFile) {
// NOTE: For both run.bat and shutdown.bat, the current working dir must be set to the script's parent dir
// (e.g. ${JBOSS_HOME}/bin) for the script to work.
processExecution.setWorkingDirectory(scriptFile.getParent());
// Both scripts require the JAVA_HOME env var to be set.
File javaHomeDir = this.serverComponent.getJavaHomePath();
if (javaHomeDir == null) {
throw new IllegalStateException("The '" + JBossASServerComponent.JAVA_HOME_PATH_CONFIG_PROP
+ "' connection property must be set in order to start or stop JBossAS via script.");
}
this.serverComponent.validateJavaHomePathProperty();
processExecution.getEnvironmentVariables().put("JAVA_HOME", javaHomeDir.getPath());
processExecution.setCaptureOutput(true);
processExecution.setWaitForCompletion(1000L); // 1 second // TODO: Should we wait longer than one second?
processExecution.setKillOnTimeout(false);
}
/**
* Shuts down the server by dispatching to shutdown via script or JMX. Waits until the server is down.
* @return The result of the shutdown operation - is successful
*/
private String shutdown() throws InterruptedException {
JBossASServerShutdownMethod shutdownMethod = Enum.valueOf(JBossASServerShutdownMethod.class,
this.serverComponent.getPluginConfiguration().getSimple(JBossASServerComponent.SHUTDOWN_METHOD_CONFIG_PROP)
.getStringValue());
String result = JBossASServerShutdownMethod.JMX.equals(shutdownMethod) ? shutdownViaJmx() : shutdownViaScript();
AvailabilityType avail = waitForServerToShutdown();
if (avail == AvailabilityType.UP) {
throw new RuntimeException("Server failed to shutdown");
} else {
return result;
}
}
/**
* Shuts down the AS server using a shutdown script.
*
* @return success message if no errors are encountered
*/
private String shutdownViaScript() {
Configuration pluginConfiguration = this.serverComponent.getPluginConfiguration();
File shutdownScriptFile = this.serverComponent.getShutdownScriptPath();
validateScriptFile(shutdownScriptFile, JBossASServerComponent.SHUTDOWN_SCRIPT_CONFIG_PROP);
String prefix = pluginConfiguration.getSimple(JBossASServerComponent.SCRIPT_PREFIX_CONFIG_PROP)
.getStringValue();
ProcessExecution processExecution = ProcessExecutionUtility.createProcessExecution(prefix, shutdownScriptFile);
initProcessExecution(processExecution, shutdownScriptFile);
String server = pluginConfiguration.getSimple(JBossASServerComponent.NAMING_URL_CONFIG_PROP).getStringValue();
if (server != null) {
processExecution.getArguments().add("--server=" + server);
}
String user = pluginConfiguration.getSimple(JBossASServerComponent.PRINCIPAL_CONFIG_PROP).getStringValue();
if (user != null) {
processExecution.getArguments().add("--user=" + user);
}
String password = pluginConfiguration.getSimple(JBossASServerComponent.CREDENTIALS_CONFIG_PROP)
.getStringValue();
if (password != null) {
processExecution.getArguments().add("--password=" + password);
}
processExecution.getArguments().add("--shutdown");
/* This tells shutdown.bat not to call the Windows PAUSE command, which
* would cause the script to hang indefinitely waiting for input.
* noinspection ConstantConditions
*/
processExecution.getEnvironmentVariables().put("NOPAUSE", "1");
if (log.isDebugEnabled()) {
log.debug("About to execute the following process: [" + processExecution + "]");
}
ProcessExecutionResults results = this.systemInfo.executeProcess(processExecution);
logExecutionResults(results);
if (results.getError() != null) {
throw new RuntimeException("Error executing shutdown script while stopping AS instance. Exit code ["
+ results.getExitCode() + "]", results.getError());
}
return "Server has been shut down.";
}
private void logExecutionResults(ProcessExecutionResults results) {
// Always log the output at info level. On Unix we could switch depending on a exitCode being !=0, but ...
log.info("Exit code from process execution: " + results.getExitCode());
log.info("Output from process execution: " + SEPARATOR + results.getCapturedOutput() + SEPARATOR);
}
/**
* Shuts down the AS server via a JMX call.
*
* @return success message if no errors are encountered
*/
private String shutdownViaJmx() {
Configuration pluginConfiguration = this.serverComponent.getPluginConfiguration();
String mbeanName = pluginConfiguration.getSimple(JBossASServerComponent.SHUTDOWN_MBEAN_CONFIG_PROP)
.getStringValue();
String operationName = pluginConfiguration.getSimple(
JBossASServerComponent.SHUTDOWN_MBEAN_OPERATION_CONFIG_PROP).getStringValue();
EmsConnection connection = this.serverComponent.getEmsConnection();
if (connection == null) {
throw new RuntimeException("Can not connect to the server");
}
EmsBean bean = connection.getBean(mbeanName);
EmsOperation operation = bean.getOperation(operationName);
/*
* Now see if we got the 'real' method (the one with no param) or the overloaded one.
* This is a workaround for a bug in EMS that prevents finding operations with same name
* and different signature.
* http://sourceforge.net/tracker/index.php?func=detail&aid=2007692&group_id=60228&atid=493495
*
* In addition, as we offer the user to specify any MBean and any method, we'd need a
* clever way for the user to specify parameters anyway.
*/
List<EmsParameter> params = operation.getParameters();
int count = params.size();
if (count == 0)
operation.invoke(new Object[0]);
else { // overloaded operation
operation.invoke(new Object[] { 0 }); // return code of 0
}
return "Server has been shut down.";
}
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.");
}
}
/**
* Restart the server by first trying a shutdown and then a start. This is fail fast.
* @return A success message on success
*/
private String restart() throws InterruptedException {
shutdown();
// Perform the restart.
start();
return "Server has been restarted.";
}
private AvailabilityType waitForServerToStart(long start) throws InterruptedException {
AvailabilityType avail;
//detect whether startWaitMax property has been set.
Configuration pluginConfig = serverComponent.getPluginConfiguration();
PropertySimple property = pluginConfig.getSimple(JBossASServerComponent.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.getPluginConfiguration();
PropertySimple property = pluginConfig.getSimple(JBossASServerComponent.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();
}
}