/*******************************************************************************
* Copyright (c) 2011 GigaSpaces Technologies Ltd. All rights reserved
*
* Licensed 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.cloudifysource.shell.commands;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteException;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.PumpStreamHandler;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.felix.gogo.commands.Argument;
import org.apache.felix.gogo.commands.Command;
import org.cloudifysource.domain.Service;
import org.cloudifysource.dsl.internal.CloudifyConstants;
import org.cloudifysource.dsl.internal.DSLException;
import org.cloudifysource.dsl.internal.ServiceReader;
import org.cloudifysource.dsl.internal.packaging.Packager;
import org.cloudifysource.dsl.internal.packaging.PackagingException;
import org.cloudifysource.dsl.utils.RecipePathResolver;
import org.cloudifysource.shell.exceptions.CLIException;
import org.cloudifysource.shell.exceptions.CLIStatusException;
import org.hyperic.sigar.Sigar;
import org.hyperic.sigar.SigarException;
import org.openspaces.pu.container.integrated.IntegratedProcessingUnitContainer;
import com.gigaspaces.internal.sigar.SigarHolder;
import com.j_spaces.kernel.Environment;
/**
* @author rafi, barakm
* @since 2.0.0
*
* Tests a recipe.
*
* Required arguments: recipe - Path to recipe folder or packaged zip file
*
* Optional arguments: recipeTimeout - Number of seconds that the recipe should run, before shutdown is invoked
* (default: 30). serviceFileName - Name of the service file in the recipe folder, if not using the default.
*
* Command syntax: test-recipe [recipeTimeout] [serviceFileName] recipe
*/
@Command(scope = "cloudify", name = "test-recipe", description = "tests a recipe")
public class TestRecipe extends AbstractGSCommand {
private static final int UNEXPECTED_ERROR_EXIT_CODE = -2;
private static final int DEFAULT_RECIPE_TIMEOUT_SECS = 30;
private static final String JAVA_HOME_ENV_VAR_NAME = "JAVA_HOME";
private static final int EXTERNAL_PROCESS_WATCHDOG_ADDITIONAL_TIMEOUT = 120;
@Argument(index = 0, required = true, name = "recipe", description = "Path to recipe folder or packaged zip file")
private File recipeFolder;
@Argument(index = 1, required = false, name = "recipeTimeout", description = "Number of seconds that the recipe"
+ " should run, before shutdown is invoked. Defaults to 30.")
private int timeout = DEFAULT_RECIPE_TIMEOUT_SECS;
@Argument(index = 2, required = false, name = "serviceFileName", description = "Name of the service file in the "
+ "recipe folder, if not using the default")
private String serviceFileName;
private static final String[] JAR_DIRS = { "lib/required", "tools/groovy/lib", "lib/platform/sigar",
"lib/optional/spring", "lib/platform/usm", "lib/platform/cloudify" };
/**
* Create a full classpath, including the existing classpath and additional paths to Jars and service files.
*
* @param serviceFolder
* The folder of the current service
* @return A full classpath
*/
private String createClasspathString(final File serviceFolder) {
// Start with current environment variable
String currentClassPathEnv = System.getenv("CLASSPATH");
if (currentClassPathEnv == null) {
currentClassPathEnv = "";
}
currentClassPathEnv += File.pathSeparator;
final StringBuilder sb = new StringBuilder(currentClassPathEnv);
// Add the required jar dirs
for (final String jarDir : JAR_DIRS) {
final File dir = getDirIfExists(jarDir);
sb.append(
dir.getAbsolutePath()).append(
File.separator).append(
"*").append(
File.pathSeparator);
}
// finally, add the service folder to the recipe, so it finds the
// META-INF files, and the lib dir
sb.append(
serviceFolder.getAbsolutePath()).append(
File.separator).append(
File.pathSeparator);
sb.append(
serviceFolder.getAbsolutePath()).append(
File.separator).append("lib").append(File.separator).append("*")
.append(File.pathSeparator);
//sb.append(serviceFolder.getAbsolutePath() + "/ext/usmlib")
// TODO - add local recipe jar files!
return sb.toString();
}
/**
* Returns the directory (as a File object) if it exists. If the directory is not found in the given location or is
* not a directory - an IllegalStateException is thrown.
*
* @param dirName
* Directory name, relative to the home directory
* @return Directory as a File object.
*/
private File getDirIfExists(final String dirName) {
final File requiredDir = new File(Environment.getHomeDirectory() + File.separator + dirName);
if (!requiredDir.exists()) {
throw new IllegalStateException("Could not find directory: " + dirName);
}
if (!requiredDir.isDirectory()) {
throw new IllegalStateException(requiredDir + " is not a directory");
}
return requiredDir;
}
/**
* {@inheritDoc}
*/
@Override
protected Object doExecute()
throws CLIException {
final RecipePathResolver pathResolver = new RecipePathResolver();
File actualRecipeFolder = null;
if (pathResolver.resolveService(this.recipeFolder)) {
actualRecipeFolder = pathResolver.getResolved();
} else {
throw new CLIStatusException("service_file_doesnt_exist",
StringUtils.join(pathResolver.getPathsLooked().toArray(), ", "));
}
File serviceFolder = null;
try {
// First Package the recipe using the regular packager
final File packagedRecipe = packageRecipe(actualRecipeFolder);
// Then unzip the package in a temp location
serviceFolder = createServiceFolder(packagedRecipe);
logger.info("Executing service in temporary folder: " + serviceFolder);
// verify that service configuration file contains a lifecycle
// closure
isServiceLifecycleNotNull(serviceFolder);
// Create the classpath environment variable
final String classpath = createClasspathString(serviceFolder);
logger.fine("Setting Test Processing Unit's Classpath to: " + classpath);
// and the command line
final CommandLine cmdLine = createCommandLine();
logger.fine("Setting Test Processing Unit's Command line to: " + cmdLine);
// Create the environment for the command, using the current one
// plus the new classpath
final Map<Object, Object> env = new HashMap<Object, Object>();
env.putAll(System.getenv());
env.put("CLASSPATH", classpath);
env.put(CloudifyConstants.GIGASPACES_CLOUD_MACHINE_ID, "localcloud");
if (!env.containsKey(JAVA_HOME_ENV_VAR_NAME)) {
final String javaHomeDirectory = getJavaHomeDirectory();
logger.warning("JAVA_HOME system variables is not set. adding JAVA_HOME with value "
+ javaHomeDirectory);
env.put(JAVA_HOME_ENV_VAR_NAME, javaHomeDirectory);
logger.info("JAVA_HOME was successfully set. added JAVA_HOME=" + javaHomeDirectory);
}
// Execute the command
final int result = executeRecipe(
cmdLine, env);
if (result != 0) {
if (result == 1) {
logger.warning("Recipe exited abnormally with exit value 1. "
+ "This may indicate that the external process did not shutdown on time and was"
+ " forcibly shutdown by the execution watchdog.");
}
throw new CLIException(getFormattedMessage(
"test_recipe_failure", result));
}
} finally {
// Delete the temporary service folder
if (serviceFolder != null) {
try {
FileUtils.deleteDirectory(serviceFolder);
} catch (final IOException e) {
logger.log(
Level.SEVERE, "Failed to delete temporary service folder: " + serviceFolder, e);
}
}
}
return getFormattedMessage("test_recipe_success");
}
/**
* Gets the java home folder through the process ID of this process.
*
* @return The path to the java folder
* @throws CLIException
* Reporting a failure to retrieve the java home directory
*/
private String getJavaHomeDirectory()
throws CLIException {
try {
final Sigar sigar = SigarHolder.getSigar();
final long thisProcessPid = sigar.getPid();
// get the java path of the current running process.
final String javaFilePath = sigar.getProcExe(
thisProcessPid).getName();
final File javaFile = new File(javaFilePath);
// Locate the java folder.
final File javaFolder = javaFile.getParentFile().getParentFile();
final String javaFolderPath = javaFolder.getAbsolutePath();
return javaFolderPath;
} catch (final SigarException e) {
throw new CLIException("Failed to set the JAVA_HOME environment variable.", e);
}
}
/**
* Verifies the service configuration is valid.
*
* @param serviceFolder
* The Folder holding the service configuration files
* @throws CLIException
* Reporting a failure to find or parse the configuration files
*/
private void isServiceLifecycleNotNull(final File serviceFolder)
throws CLIException {
Service service;
try {
final File serviceFileDir = new File(serviceFolder, "ext");
service = ServiceReader.getServiceFromDirectory(serviceFileDir).getService();
if (service.getLifecycle() == null) {
throw new CLIException(getFormattedMessage("test_recipe_service_lifecycle_missing"));
}
} catch (final DSLException e) {
logger.log(
Level.SEVERE, "DSL Parsing failed: " + e.getMessage(), e);
e.printStackTrace();
throw new CLIException("Packaging failed: " + e.getMessage(), e);
}
}
/**
* This inner class reads and prints filtered text from a given source (BufferedReader). verbose mode - turns off
* the filtering of the data
*/
private static class FilteredOutputHandler implements Runnable {
private final BufferedReader reader;
private final boolean verbose;
private static final String[] FILTERS = { "org.cloudifysource.dsl.internal.BaseServiceScript",
"org.cloudifysource.usm.launcher.DefaultProcessLauncher", "org.cloudifysource.usm.USMEventLogger",
"-Output]", "-Error]"
};
/**
* Constructor.
*
* @param reader
* This is the source text will be read from before filtering and possibly printing it.
* @param verbose
* Sets filtering off when this value is true.
*/
FilteredOutputHandler(final BufferedReader reader, final boolean verbose) {
this.reader = reader;
this.verbose = verbose;
}
/**
* {@inheritDoc}
*/
@Override
public void run() {
while (true) {
try {
final String line = reader.readLine();
if (line == null) {
return;
}
if (this.verbose) {
System.out.println(line);
} else {
for (final String filter : FILTERS) {
if (line.contains(filter)) {
System.out.println(line);
}
}
}
} catch (final IOException e) {
return;
}
}
}
}
/**
* Execute a command line in with a given map of environment settings. The execution outupt is filtered unless
* verbose is set to true.
*
* @param cmdLine
* The command to execute
* @param env
* Environment settings available for the command execution
* @return the command's execution exit code, or -2 if the command failed to execute
*/
private int executeRecipe(final CommandLine cmdLine, final Map<Object, Object> env) {
final DefaultExecutor executor = new DefaultExecutor();
// The watchdog will terminate the process if it does not end within the
// specified timeout
final int externalProcessTimeout = (this.timeout + EXTERNAL_PROCESS_WATCHDOG_ADDITIONAL_TIMEOUT) * 1000;
final ExecuteWatchdog watchdog = new TestRecipeWatchdog(externalProcessTimeout);
executor.setWatchdog(watchdog);
executor.setExitValue(0);
int result = -1;
PipedInputStream in = null;
PipedOutputStream out = null;
BufferedReader reader = null;
try {
in = new PipedInputStream();
out = new PipedOutputStream(in);
reader = new BufferedReader(new InputStreamReader(in));
final Thread thread = new Thread(new FilteredOutputHandler(reader, this.verbose));
thread.setDaemon(true);
thread.start();
final PumpStreamHandler psh = new PumpStreamHandler(out, out);
executor.setStreamHandler(psh);
result = executor.execute(
cmdLine, env);
} catch (final ExecuteException e) {
logger.log(
Level.SEVERE, "A problem was encountered while executing the recipe: " + e.getMessage(), e);
} catch (final IOException e) {
logger.log(
Level.SEVERE, "An IO Exception was encountered while executing the recipe: " + e.getMessage(), e);
result = UNEXPECTED_ERROR_EXIT_CODE;
}
return result;
}
/**
* Extracts the given file to a service folder.
*
* @param packagedRecipe
* The file to extract
* @return The service folder
*/
private File createServiceFolder(final File packagedRecipe) {
File serviceFolder = null;
try {
serviceFolder = unzipFile(packagedRecipe);
} catch (final IOException e) {
throw new IllegalStateException("Failed to unzip recipe: " + e.getMessage(), e);
}
return serviceFolder;
}
/**
* Packages the recipe folder. If the recipe folder is a file, verifies it is a zip or a jar.
* @param actualRecipeFolder
*
* @return Packaged recipe folder
* @throws CLIException
* Reporting missing recipe folder, wrong file type or failure to pack the folder
*/
private File packageRecipe(final File actualRecipeFolder)
throws CLIException {
if (!actualRecipeFolder.exists()) {
throw new CLIStatusException(
"service_file_doesnt_exist", actualRecipeFolder.getAbsolutePath(), this.serviceFileName);
}
if (actualRecipeFolder.isFile()) {
if (actualRecipeFolder.getName().endsWith(
".zip") || actualRecipeFolder.getName().endsWith(
".jar")) {
return actualRecipeFolder;
}
throw new CLIStatusException("not_jar_or_zip", actualRecipeFolder.getAbsolutePath(), this.serviceFileName);
}
// it's a folder
File dslDirOrFile = actualRecipeFolder;
if (serviceFileName != null) {
// use non default service file
dslDirOrFile = new File(dslDirOrFile, serviceFileName);
}
return doPack(dslDirOrFile);
}
/**
* Packages the recipe files and other required files in a zip.
*
* @param recipeDirOrFile
* The recipe service DSL file or containing folder
* @return A zip file
* @throws CLIException
* Reporting a failure to find or parse the given DSL file, or pack the zip file
*/
public File doPack(final File recipeDirOrFile)
throws CLIException {
try {
return Packager.pack(recipeDirOrFile);
} catch (final IOException e) {
throw new CLIException(e);
} catch (final PackagingException e) {
throw new CLIException(e);
} catch (final DSLException e) {
throw new CLIException(e);
}
}
/**
* Create a complete command line, including path and arguments.
*
* @return Configured command line, ready for execution
*/
private CommandLine createCommandLine() {
final String javaPath = getJavaPath();
final String gsHome = Environment.getHomeDirectory();
final String[] commandParams =
{ "-Dcom.gs.home=" + gsHome, "-Dorg.hyperic.sigar.path=" + gsHome + "/lib/platform/sigar",
"-D" + CloudifyConstants.TEST_RECIPE_TIMEOUT_SYSPROP + "=" + timeout,
IntegratedProcessingUnitContainer.class.getName(), "-cluster", "id=1", "total_members=1" };
final CommandLine cmdLine = new CommandLine(javaPath);
for (final String param : commandParams) {
cmdLine.addArgument(param);
}
if (this.serviceFileName != null) {
cmdLine.addArgument("-properties");
cmdLine.addArgument("embed://" + CloudifyConstants.CONTEXT_PROPERTY_SERVICE_FILE_NAME + "="
+ this.serviceFileName);
}
// -Dcom.gs.usm.RecipeShutdownTimeout=10
return cmdLine;
}
/**
* Gets java path via sigar from the current process.
*
* @return java path
*/
private String getJavaPath() {
final long pid = SigarHolder.getSigar().getPid();
try {
return SigarHolder.getSigar().getProcExe(
pid).getName();
} catch (final SigarException e) {
throw new IllegalStateException("Failed to read java path via sigar from current process (" + pid + ")", e);
}
}
/**
* Creates a temporary folder.
*
* @return Temporary folder
* @throws IOException
* Reporting a failure to create the folder
*/
protected static File createTempDir()
throws IOException {
File targetDir = null;
final String tmpDir = System.getProperty("java.io.tmpdir");
if (tmpDir == null) {
throw new IllegalStateException(
"The java.io.tmpdir property is null. Can't create a temporary directory for service unpacking");
}
if (tmpDir.indexOf(' ') >= 0) {
targetDir = new File("Recipe_Test_Temp_Files" + File.separator + "Test_" + System.currentTimeMillis());
if (!targetDir.mkdirs()) {
throw new IllegalStateException(
"Failed to create a directory where service will be unzipped. Target was: " + targetDir);
}
logger.warning("The System temp directory name includes spaces. Using alternate directory: " + targetDir);
}
final File tempFile = File.createTempFile(
"GS_tmp_dir", ".service", targetDir);
final String path = tempFile.getAbsolutePath();
tempFile.delete();
tempFile.mkdirs();
final File baseDir = new File(path);
return baseDir;
}
/**
* Unzips a given file.
*
* @param inputFile
* The zip file to extract
* @return The new folder, containing the extracted content of the zip file
* @throws IOException
* Reporting a failure to extract the zipped file or close it afterwards
*/
private static File unzipFile(final File inputFile)
throws IOException {
ZipFile zipFile = null;
try {
final File baseDir = TestRecipe.createTempDir();
zipFile = new ZipFile(inputFile);
final Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
final ZipEntry entry = entries.nextElement();
if (entry.isDirectory()) {
logger.fine("Extracting directory: " + entry.getName());
final File dir = new File(baseDir, entry.getName());
dir.mkdir();
continue;
}
logger.finer("Extracting file: " + entry.getName());
final File file = new File(baseDir, entry.getName());
file.getParentFile().mkdirs();
ServiceReader.copyInputStream(
zipFile.getInputStream(entry), new BufferedOutputStream(new FileOutputStream(file)));
}
return baseDir;
} finally {
if (zipFile != null) {
try {
zipFile.close();
} catch (final IOException e) {
logger.log(
Level.SEVERE, "Failed to close zip file after unzipping zip contents", e);
}
}
}
}
/***********
* Workaround accessor to prevent eclipse clean up from turning the timeout field to a final field.
*
* @param timeout
* .
*/
void setTimeout(final int timeout) {
this.timeout = timeout;
}
}