fr.ens.biologie.genomique.eoulsan.it.ITCommandExecutor.java Source code

Java tutorial

Introduction

Here is the source code for fr.ens.biologie.genomique.eoulsan.it.ITCommandExecutor.java

Source

/*
 *                  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 suprieure 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();
        }
    }
}