/* * Eoulsan development code * * This code may be freely distributed and modified under the * terms of the GNU Lesser General Public License version 2.1 or * later and CeCILL-C. This should be distributed with the code. * If you do not have a copy, see: * * http://www.gnu.org/licenses/lgpl-2.1.txt * http://www.cecill.info/licences/Licence_CeCILL-C_V1-en.txt * * Copyright for this code is held jointly by the Genomic platform * of the Institut de Biologie de l'École normale supérieure and * the individual authors. These should be listed in @author doc * comments. * * For more information on the Eoulsan project and its aims, * or to join the Eoulsan Google group, visit the home page * at: * * http://outils.genomique.biologie.ens.fr/eoulsan * */ package fr.ens.biologie.genomique.eoulsan.it; import static com.google.common.base.Preconditions.checkNotNull; import static fr.ens.biologie.genomique.eoulsan.EoulsanLogger.getLogger; import static fr.ens.biologie.genomique.eoulsan.util.StringUtils.toTimeHumanReadable; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.List; import java.util.Properties; import java.util.concurrent.TimeUnit; import org.apache.commons.compress.utils.Charsets; import com.google.common.base.Stopwatch; import fr.ens.biologie.genomique.eoulsan.EoulsanException; /** * The class represents an executor to command line. * @author Sandrine Perrin * @since 2.0 */ public class ITCommandExecutor { /** The Constant STDERR_FILENAME. */ private static final String STDERR_FILENAME = "STDERR"; /** The Constant STDOUT_FILENAME. */ private static final String STDOUT_FILENAME = "STDOUT"; /** The Constant CMDLINE_FILENAME. */ private static final String CMDLINE_FILENAME = "CMDLINE"; /** The test conf. */ private final Properties testConf; /** The output test directory. */ private final File outputTestDirectory; /** The duration max. */ private final int durationMax; /** The cmd line file. */ private final File cmdLineFile; // Compile current environment variable and set in configuration file with // prefix PREFIX_ENV_VAR /** The environment variables. */ private final String[] environmentVariables; /** * Execute a script from a command line retrieved from the test configuration. * @param scriptConfKey key for configuration to get command line * @param suffixNameOutputFile suffix for standard and error output file on * process * @param desc description on command line * @param isApplicationCmdLine true if application to run, otherwise false * corresponding to annexes script * @return result of execution command line, if command line not found in * configuration return null */ public ITCommandResult executeCommand(final String scriptConfKey, final String suffixNameOutputFile, final String desc, final boolean isApplicationCmdLine) { if (this.testConf.getProperty(scriptConfKey) == null) { return null; } // Get command line from the configuration final String cmdLine = this.testConf.getProperty(scriptConfKey); if (cmdLine.isEmpty()) { return null; } // Save command line in file if (isApplicationCmdLine) { try { com.google.common.io.Files.write(cmdLine + "\n", this.cmdLineFile, Charsets.UTF_8); } catch (final IOException e) { getLogger().warning( "Error while writing the application command line in file: " + e.getMessage()); } } // Define stdout and stderr file final File stdoutFile = createSdtoutFile(suffixNameOutputFile); final File stderrFile = createSdterrFile(suffixNameOutputFile); int exitValue = -1; final Stopwatch timer = Stopwatch.createStarted(); final ITCommandResult cmdResult = new ITCommandResult(cmdLine, this.outputTestDirectory, desc, durationMax); try { final Process p = Runtime.getRuntime().exec(cmdLine, this.environmentVariables, this.outputTestDirectory); // Init monitor and start final MonitorThread monitor = new MonitorThread(p, desc, durationMax); // Save stdout if (stdoutFile != null) { new CopyProcessOutput(p.getInputStream(), stdoutFile, "stdout").start(); } // Save stderr if (stderrFile != null) { new CopyProcessOutput(p.getErrorStream(), stderrFile, "stderr").start(); } // Wait the end of the process exitValue = p.waitFor(); // Stop monitor thread monitor.interrupt(); cmdResult.setExitValue(exitValue); // Execution script fail, create an exception if (exitValue != 0) { if (monitor.isKilledProcess()) { cmdResult.asInterruptedProcess(); cmdResult.setException( new EoulsanException("\tKill process.\n\tCommand line: " + cmdLine + "\n\tDirectory: " + this.outputTestDirectory + "\n\tMessage: " + monitor.getMessage())); } else { cmdResult.setException(new EoulsanException("\tCommand line: " + cmdLine + "\n\tDirectory: " + this.outputTestDirectory + "\n\tMessage: bad exit value: " + exitValue)); cmdResult.setErrorFileOnProcess(stderrFile); } } else if (exitValue == 0 && !isApplicationCmdLine) { // Success execution, remove standard and error output file if (!stdoutFile.delete()) { getLogger().warning("Unable to deleted stdout file: " + stdoutFile); } if (!stderrFile.delete()) { getLogger().warning("Unable to deleted stderr file: " + stdoutFile); } } } catch (IOException | InterruptedException e) { cmdResult.setException(e, "\tError before execution.\n\tCommand line: " + cmdLine + "\n\tDirectory: " + this.outputTestDirectory + "\n\tMessage: " + e.getMessage()); } finally { cmdResult.setDuration(timer.elapsed(TimeUnit.MILLISECONDS)); timer.stop(); } return cmdResult; } /** * Create standard output file with suffix name, if not empty. * @param suffixName the suffix name * @return file */ private File createSdtoutFile(final String suffixName) { return new File(this.outputTestDirectory, STDOUT_FILENAME + (suffixName.isEmpty() ? "" : "_" + suffixName)); } /** * Create error output file with suffix name, if not empty. * @param suffixName the suffix name * @return file */ private File createSdterrFile(final String suffixName) { return new File(this.outputTestDirectory, STDERR_FILENAME + (suffixName.isEmpty() ? "" : "_" + suffixName)); } // // Constructor // /** * Public constructor. * @param testConf properties on the test * @param outputTestDirectory output test directory * @param environmentVariables environment variables to run test * @param durationMax the duration maximum in minutes */ public ITCommandExecutor(final Properties testConf, final File outputTestDirectory, final List<String> environmentVariables, final int durationMax) { this.testConf = testConf; this.outputTestDirectory = outputTestDirectory; // Extract environment variable from current context and configuration test this.environmentVariables = environmentVariables.toArray(new String[environmentVariables.size()]); this.cmdLineFile = new File(this.outputTestDirectory, CMDLINE_FILENAME); this.durationMax = durationMax; } /** * This internal class allow to save Process outputs. * @author Laurent Jourdren */ private static final class CopyProcessOutput extends Thread { /** The path. */ private final Path path; /** The in. */ private final InputStream in; /** The desc. */ private final String desc; /* * (non-Javadoc) * @see java.lang.Thread#run() */ @Override public void run() { try { Files.copy(this.in, this.path, StandardCopyOption.REPLACE_EXISTING); } catch (final IOException e) { getLogger().warning( "Error while copying " + this.desc + ": " + e.getMessage()); } } /** * Instantiates a new copy process output. * @param in the in * @param file the file * @param desc the desc */ CopyProcessOutput(final InputStream in, final File file, final String desc) { checkNotNull(in, "in argument cannot be null"); checkNotNull(file, "file argument cannot be null"); checkNotNull(desc, "desc argument cannot be null"); this.in = in; this.path = file.toPath(); this.desc = desc; } } // // Internal class // /** * This class create a monitor thread on script process to stop him if * overtake runtime maximum period set in configuration file or use default * value in ITFactory class. * @author Sandrine Perrin * @since 2.0 */ private static final class MonitorThread extends Thread { /** Script process. */ private final Process p; /** PID on the process. */ private int pid = -1; /** Duration max in minutes. */ private final int durationMaxInMinutes; /** Description on script. */ private final String desc; /** Process is killed . */ private boolean killedProcess = false; /** Process is kill by command unix. */ private boolean killByCmd = false; /** Process is kill by method destroy on process. */ private boolean killByMethodDestroy = false; public void run() { // try { // // TODO // System.out.println("start monitor on script " + desc); // // // Sleep // sleep(durationMaxInMinutes * 60 * 1000); // // System.out.println("end period allowed"); // } catch (InterruptedException e) { // // TODO Auto-generated catch block // // e.printStackTrace(); // } // // // Destroy process if always running // destroyProcessScript(); } /** * Destroy process script, in first step use command Unix kill -9, if it is * failed call destroy method from process class. */ void destroyProcessScript() { // try { // killedProcess = true; // killByCmd = true; // // // Retrieve PID on children process (corresponding to java runtime pid // // for Eoulsan) DON'T WORK // final String n = // ProcessUtils.execToString("ps x -o \"%p %r %y %x %c \" | grep \"^" // + this.pid + "\" | cut -f 2 -d ' '"); // // System.out.println("CMD KILL: kill -TERM " + pid + " " + n); // // Kill process // ProcessUtils.exec("kill -TERM " + pid + " " + n, true); // // // TODO // System.out.println("process was killed"); // // } catch (Throwable e) { // // killedProcess = true; // killByMethodDestroy = true; // // // Destroy process // this.p.destroy(); // } } /** * Gets the pid. * @return the pid */ int getPID() { try { Field f = p.getClass().getDeclaredField("pid"); f.setAccessible(true); return (int) f.get(p); } catch (Throwable e) { e.printStackTrace(); } return -1; } // // Getter // /** * Checks if is killed process. * @return true, if is killed process */ public boolean isKilledProcess() { return killedProcess; } /** * Gets the message for report. * @return the message */ public String getMessage() { return "script process on " + this.desc + " with pid " + pid + " has been killed " + (killByCmd ? "by command kill -TERM" : (killByMethodDestroy ? "by method process.destroy" : "other mean")) + " after " + toTimeHumanReadable(this.durationMaxInMinutes * 60 * 1000); } /** * Constructor. * @param processScript the process on script * @param desc the description script * @param durationMaxInMinutes the duration maximum in minutes */ public MonitorThread(final Process processScript, final String desc, final int durationMaxInMinutes) { this.p = processScript; this.desc = desc; this.durationMaxInMinutes = durationMaxInMinutes; this.pid = getPID(); // Start thread this.start(); } } }