All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.devonfw.cobigen.impl.externalprocess.ExternalProcessHandler Maven / Gradle / Ivy

There is a newer version: 2021.12.006
Show newest version
package com.devonfw.cobigen.impl.externalprocess;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.PosixFilePermission;
import java.util.Properties;
import java.util.zip.GZIPInputStream;

import javax.xml.bind.UnmarshalException;

import org.apache.commons.compress.archivers.ArchiveException;
import org.apache.commons.compress.archivers.ArchiveStreamFactory;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.commons.compress.utils.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.devonfw.cobigen.api.constants.ExternalProcessConstants;
import com.devonfw.cobigen.api.exception.CobiGenRuntimeException;
import com.devonfw.cobigen.impl.exceptions.ConnectionExceptionHandler;
import com.devonfw.cobigen.impl.exceptions.ConnectionExceptionHandler.ConnectionException;
import com.devonfw.cobigen.impl.exceptions.HttpConnectionException;
import com.devonfw.cobigen.impl.util.ExceptionUtil;
import com.devonfw.cobigen.impl.util.ProcessOutputUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;

/**
 * Class for handling the external process creation and the communication with it.
 *
 */
public class ExternalProcessHandler {

    /** Logger instance. */
    private static final Logger LOG = LoggerFactory.getLogger(ExternalProcessHandler.class);

    /**
     * Port used for connecting to the server
     */
    private Integer port;

    /**
     * Host name of the server, by default is localhost
     */
    private String hostName;

    /**
     * HTTP connection instance for sending requests
     */
    private HttpURLConnection conn;

    /**
     * Native process instance
     */
    private Process process;

    /**
     * Used for handling all the errors our external process throws
     */
    private ProcessOutputUtil errorHandler = null;

    /**
     * URL for connecting with the different services of our external process
     */
    private URL url;

    /**
     * Path of the executable file (the server)
     */
    private String exeName = "";

    /**
     * Singleton instance of {@link ExternalProcessHandler}.
     */
    public static ExternalProcessHandler externalProcessHandler = null;

    /**
     * Exception handler related to connectivity to the server
     */
    private ConnectionExceptionHandler connectionExc = new ConnectionExceptionHandler();

    /**
     * Holds properties of the external process like file name, path, URL to download...
     */
    private ExternalProcessProperties processProperties;

    /**
     * Using singleton pattern, we will only have one instance of {@link ExternalProcessHandler}.
     *
     * @param exeName
     *            of the executable file to execute
     * @param hostName
     *            name of the server, normally localhost
     * @param port
     *            port to be used for connecting to the server
     * @return external process instance
     */
    public static ExternalProcessHandler getExternalProcessHandler(String exeName, String hostName, Integer port) {

        if (externalProcessHandler == null) {
            externalProcessHandler = new ExternalProcessHandler(exeName, hostName, port);
        }

        return externalProcessHandler;
    }

    /**
     * Using singleton pattern, we will only have one instance of {@link ExternalProcessHandler}.
     *
     * @param activatorClass
     *            plug-in class that activated the execution of the server. We need it to find the jar of the
     *            activator class, which should define the executable server in a properties file
     * @param hostName
     *            name of the server, normally localhost
     * @param port
     *            port to be used for connecting to the server
     * @return external process instance
     */
    public static ExternalProcessHandler getExternalProcessHandler(Class activatorClass, String hostName,
        Integer port) {

        if (externalProcessHandler == null) {
            externalProcessHandler = new ExternalProcessHandler(activatorClass, hostName, port);
        }

        return externalProcessHandler;
    }

    /**
     * Constructor of {@link ExternalProcessHandler}.
     *
     * @param name
     *            of the executable file to execute
     * @param hostName
     *            name of the server, normally localhost
     * @param port
     *            port to be used for connecting to the server
     */
    protected ExternalProcessHandler(String name, String hostName, int port) {

        LOG.info("Instantiating server: " + name);
        exeName = name;
        processProperties = new ExternalProcessProperties(exeName);
        this.hostName = hostName;
        this.port = port;
    }

    /**
     * Constructor of {@link ExternalProcessHandler}.
     *
     * @param activatorClass
     *            plug-in class that activated the execution of the server. We need it to find the jar of the
     *            activator class, which should define the executable server in a properties file
     * @param hostName
     *            name of the server, normally localhost
     * @param port
     *            port to be used for connecting to the server
     */
    public ExternalProcessHandler(Class activatorClass, String hostName, Integer port) {
        setServerProperties(activatorClass);
        this.hostName = hostName;
        this.port = port;
    }

    /**
     * Tries to execute, as a new process, the executable file defined inside the plugin calling this method.
     * Also initializes the error handler.
     *
     * @param activatorClass
     *            plug-in class that activated the execution of the server. We need it to find the jar of the
     *            activator class, which should define the executable server in a properties file
     */
    public void setServerProperties(Class activatorClass) {

        try {
            // We load the properties file from the input stream
            if (activatorClass.getResourceAsStream("/META-INF/externalservers/server.properties") != null) {

                Properties serverProperties = new Properties();
                serverProperties
                    .load(activatorClass.getResourceAsStream("/META-INF/externalservers/server.properties"));

                processProperties = new ExternalProcessProperties(serverProperties);
                exeName = processProperties.getFileName();

            } else {
                throw new CobiGenRuntimeException(
                    "Error starting the external process. The server.properties file has not been found");
            }

        } catch (IOException e) {
            throw new CobiGenRuntimeException(
                "Error starting the external process. Unable to load server.properties file.", e);
        }
    }

    /**
     * Executes the exe file
     * @return true only if the exe has been correctly executed
     */
    public boolean startServer() {

        try {
            String filePath = processProperties.getFilePath();
            if (exeIsNotValid(filePath)) {
                filePath = downloadExe(processProperties.getDownloadURL(), filePath, processProperties.getFileName());
            }

            setPermissions(filePath);

            process = new ProcessBuilder(filePath, String.valueOf(port)).start();

            // We try to get the error output
            errorHandler = new ProcessOutputUtil(process.getErrorStream(), "UTF-8");

            int retry = 0;
            while (!process.isAlive() && retry <= 10) {
                if (processHasErrors()) {
                    terminateProcessConnection();
                    // If the process outputs errors, it means that we were able to start it successfully,
                    // that's why we return true
                    return true;
                }
                retry++;
                // This means the executable has already finished
                if (process.exitValue() == 0) {
                    return true;
                }
                LOG.info("Waiting process to be alive");
            }
            if (retry > 10) {
                // We were not able to start the process
                return false;
            }
        } catch (IOException e) {
            LOG.error("Unable to start the exe/server", e);
        }

        return true;
    }

    /**
     * Returns true if the current exe server is not valid and we need to force a download.
     * @param filePath
     *            path to the exe of the server
     * @return true if the exe file needs to be downloaded again
     */
    private boolean exeIsNotValid(String filePath) {
        File exeFile = new File(filePath);
        if (!exeFile.isFile()) {
            return true;
        }

        try {
            if (removeOldVersions(exeName, exeFile.getName())) {
                LOG.info(
                    "Cleaning up the external servers folder because something strange was found. Server: " + exeName);
                Files.deleteIfExists(Paths.get(filePath));
                return true;
            }
        } catch (IOException e) {
            LOG.error(
                "Not able to clean externalservers folder, but we will keep the execution as this is not a blocker", e);
        }
        return false;
    }

    /**
     * Sets permissions to the executable file, so that it can be executed
     * @param filePath
     *            path to the file we want to change its permissions
     * @throws IOException
     *             throws {@link IOException}
     */
    private void setPermissions(String filePath) throws IOException {
        // Depending on the operative system, we need to set permissions in a different way
        if (processProperties.getOsName().indexOf("win") >= 0) {
            File exeFile = new File(filePath);
            try {
                exeFile.setExecutable(true, false);
            } catch (SecurityException e) {
                LOG.error("Not able to set executable permissions on the file", e);
            }
        } else {
            Files.setPosixFilePermissions(Paths.get(filePath),
                Sets.newHashSet(PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.OWNER_READ,
                    PosixFilePermission.GROUP_EXECUTE, PosixFilePermission.GROUP_READ,
                    PosixFilePermission.OTHERS_EXECUTE, PosixFilePermission.OTHERS_READ));
        }
    }

    /**
     * Downloads the external server on the specified folder (which will normally be .cobigen folder)
     * @param downloadURL
     *            the URL that points to the file to download
     * @param filePath
     *            path where the external server should be downloaded to
     * @param fileName
     *            name of the external server
     * @return path of the external server
     * @throws IOException
     *             {@link IOException} occurred while downloading the file
     */
    private String downloadExe(String downloadURL, String filePath, String fileName) throws IOException {

        String tarFileName = "";
        String currentFileName = "";
        File exeFile = new File(filePath);
        String parentDirectory = exeFile.getParent();

        URL url = new URL(downloadURL);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("GET");
        conn.connect();

        // Downloading tar file
        File tarFile;
        Path tarPath;
        LOG.info("Downloading server...");
        try (InputStream inputStream = conn.getInputStream()) {

            tarFileName = conn.getURL().getFile().substring(conn.getURL().getFile().lastIndexOf("/") + 1);
            tarFile = new File(parentDirectory + File.separator + tarFileName);
            tarPath = tarFile.toPath();
            if (!tarFile.exists()) {
                Files.copy(inputStream, tarPath, StandardCopyOption.REPLACE_EXISTING);
            }
        }
        conn.disconnect();

        // We need to extract the file to our current directory
        // Do we have write access?
        if (Files.isWritable(Paths.get(parentDirectory))) {

            LOG.info("Extracting server to users folder...");
            try (InputStream is = new GZIPInputStream(new FileInputStream(tarPath.toString()));
                TarArchiveInputStream tarInputStream =
                    (TarArchiveInputStream) new ArchiveStreamFactory().createArchiveInputStream("tar", is);) {

                TarArchiveEntry entry;

                while ((entry = tarInputStream.getNextTarEntry()) != null) {
                    if (entry.isDirectory()) {
                        continue;
                    }
                    if (entry.getName().contains(fileName)) {
                        // We don't want the directories (src/main/server.exe), we just want to create the
                        // file (server.exe)
                        currentFileName = getLastPartOfTarPath(entry.getName());
                        File curfile = new File(parentDirectory, currentFileName);

                        try (FileOutputStream fos = new FileOutputStream(curfile)) {
                            IOUtils.copy(tarInputStream, fos);

                            fos.flush();
                            // We need to wait until it has finished writing the file
                            fos.getFD().sync();
                            break;
                        }
                    }
                }

            } catch (ArchiveException e) {
                throw new CobiGenRuntimeException("Error while extracting the external server.", e);
            }
        } else {
            // We are not able to extract the server
            Files.deleteIfExists(tarPath);
            return "";
        }

        // Remove tar file
        Files.deleteIfExists(tarPath);
        // Now we remove old versions of this file
        removeOldVersions(fileName, currentFileName);
        return filePath;

    }

    /**
     * Returns the last part of a tar path (path inside a tar file). So if we have "src/test/java" it will
     * return "java"
     * @param path
     *            to perform the operation
     * @return string with the result
     */
    private String getLastPartOfTarPath(String path) {
        return path.substring(path.lastIndexOf("/") + 1);
    }

    /**
     * If found, remove all the files that contain this file name. This is used for removing not needed
     * versions of the current external process.
     * @param fileName
     *            name of the external server (e.g. "nestserver")
     * @param currentFileName
     *            name of the current external server including the version number and its extension (e.g.
     *            "nestserver-1.0.7.exe")
     * @return true if any file was removed
     * @throws IOException
     *             {@link IOException} occurred while removing the file
     */
    private Boolean removeOldVersions(String fileName, String currentFileName) throws IOException {
        File folder = new File(ExternalProcessConstants.EXTERNAL_PROCESS_FOLDER.toString());
        File[] listOfFiles = folder.listFiles();
        Boolean somethingWasRemoved = false;

        for (File currFile : listOfFiles) {
            if (currFile.getName().contains(fileName) && !currFile.getName().equals(currentFileName)) {
                // Remove old version of file
                Files.deleteIfExists(currFile.toPath());
                somethingWasRemoved = true;
            }
        }
        return somethingWasRemoved;
    }

    /**
     * Tries several times to start a new connection to the server. If the port is already in use, tries to
     * connect again to another port
     *
     * @return true if the ExternalProcess was able to connect to the server
     */
    public boolean initializeConnection() {

        for (int retry = 0; retry < 10; retry++) {
            try {
                startConnection();

                // Just check correct port acquisition
                if (acquirePort()) {
                    return true;
                }
            } catch (Exception e) {
                LOG.error("Connection to server failed, attempt number " + retry + ".");
                try {
                    LOG.info("Sleeping...");
                    Thread.sleep(100);
                } catch (InterruptedException interrupted) {
                    LOG.error("Error while trying to sleep the execution.");
                }
                if (connectionExc.handle(e).equals(ConnectionException.MALFORMED_URL)) {
                    LOG.error("MalformedURLException: Connection to server failed, MalformedURL.", e);
                    return false;
                }

            } finally {
                conn.disconnect();
            }
        }
        return false;
    }

    /**
     * Tries to start a new connection to the server
     *
     * @throws MalformedURLException
     *             thrown when the URL is malformed
     * @throws IOException
     *             throws {@link IOException}
     */
    public void startConnection() throws IOException {

        url = new URL(
            "http://" + hostName + ":" + port + "/processmanagement/" + processProperties.getPluginName() + "/");
        conn = (HttpURLConnection) url.openConnection();
        conn.connect();
    }

    /**
     * Tries to acquire a port. If the port is already in use, tries to execute again the external process
     * with another port
     *
     * @return true if a port has been acquired
     */
    private boolean acquirePort() {

        if (isNotConnected()) {
            restartServer();
        } else {
            return true;
        }
        return false;
    }

    /**
     * Checks whether the external process error handler has any content. If so, it means there is an error
     *
     * @return true if external process contains errors
     */
    private boolean processHasErrors() {

        if (errorHandler != null) {
            // External process has not printed any error
            if (errorHandler.getText().isEmpty()) {
                return false;
            }
        }

        return true;
    }

    /**
     * Sends a dummy request to the server in order to check if it is not connected
     *
     * @return true if it is not connected
     */
    public boolean isNotConnected() {

        try {
            getConnection("GET", "Content-Type", "text/plain", ExternalProcessConstants.IS_CONNECTION_READY);
            if (conn.getResponseCode() < 300) {
                return false;
            }
        } catch (IOException e) {
            LOG.error("Connection to server failed, maybe the server is not yet deployed...");
        }

        return true;
    }

    /**
     * Gets an HTTP request using the specified parameters
     *
     * @param httpMethod
     *            HTTP method to use (POST, GET, PUT...)
     * @param headerProperty
     *            Header property to use (content-type, content-length
     * @param mediaType
     *            type of media (application/json, text/plain...)
     * @param endpointURL
     *            The endpoint URL of the service
     * @return the {@link HttpURLConnection} to the endpoint
     */
    public HttpURLConnection getConnection(String httpMethod, String headerProperty, String mediaType,
        String endpointURL) {

        try {
            URL currentURL = new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getFile() + endpointURL);

            conn = (HttpURLConnection) currentURL.openConnection();

            conn.setDoOutput(true);
            conn.setRequestMethod(httpMethod);
            conn.setRequestProperty(headerProperty, mediaType);
            conn.setConnectTimeout(ExternalProcessConstants.CONNECTION_TIMEOUT);
            conn.setReadTimeout(ExternalProcessConstants.CONNECTION_TIMEOUT);

        } catch (Exception e) {
            Throwable parseCause = ExceptionUtil.getCause(e, Exception.class, UnmarshalException.class);
            LOG.info(parseCause.toString(), e);
            ConnectionExceptionHandler connectionExc = new ConnectionExceptionHandler();
            connectionExc.handle(e);
        }
        return conn;
    }

    /**
     * Sends a transfer object to the server, by using http connection
     * @param dataTo
     *            Data to transfer, it should be a serializable class
     * @param conn
     *            {@link HttpURLConnection} to the server, containing also the URL
     * @param charsetName
     *            the name of a supported {@link Charset}
     * @return returns true when the status code of the request was 200 or 201
     */
    public boolean sendRequest(Object dataTo, HttpURLConnection conn, String charsetName) {
        ObjectWriter objWriter;
        objWriter = new ObjectMapper().writer().withDefaultPrettyPrinter();
        String jsonMergerTo;
        try (OutputStream os = conn.getOutputStream();
            OutputStreamWriter osw = new OutputStreamWriter(os, Charset.forName(charsetName).newEncoder());) {

            jsonMergerTo = objWriter.writeValueAsString(dataTo);
            // We need to escape new lines because otherwise our JSON gets corrupted
            jsonMergerTo = jsonMergerTo.replace("\\n", "\\\\n");

            osw.write(jsonMergerTo);
            osw.flush();

            for (int i = 0; i < ExternalProcessConstants.NUMBER_OF_RETRIES; i++) {
                conn.connect();
                int statusCode = conn.getResponseCode();

                if (statusCode == HttpURLConnection.HTTP_OK || statusCode == HttpURLConnection.HTTP_CREATED) {
                    // Successful request
                    return true;
                }
                if (shouldNotRetry(conn.getResponseCode())) {
                    LOG.error("Sending the request failed. The response message is the following: "
                        + conn.getResponseMessage());
                    return false;
                }
            }
            return false;
        } catch (IOException e) {
            Throwable parseCause = ExceptionUtil.getCause(e, Exception.class, UnmarshalException.class);
            LOG.info(parseCause.toString(), e);
        }

        return false;

    }

    /**
     * Checks whether the HTTP request should NOT be retried
     * @param statusCode
     *            code of the HTTP request
     * @return true when we should not retry because the status code describes a unavoidable case
     * @throws IOException
     *             throws {@link IOException}
     */
    private boolean shouldNotRetry(int statusCode) throws IOException {

        switch (statusCode) {

        case HttpURLConnection.HTTP_MOVED_TEMP:
            return false;

        case HttpURLConnection.HTTP_UNAVAILABLE:
            return false;

        default:
            try {
                throw new HttpConnectionException("Failed : HTTP error code : " + conn.getResponseCode());
            } catch (HttpConnectionException e) {
                return true;
            }
        }

    }

    /**
     * Terminates the current connection to the server, the process and the error handler. Then tries to load
     * a new server on a different port number
     */
    public void restartServer() {
        terminateProcessConnection();
        port++;
        startServer();
    }

    /**
     * Terminates the connection to the server (if was still connected), the process (if it was still alive)
     * and the error output handler (if it was still alive)
     * @return true if it was able to terminate the process
     */
    public Boolean terminateProcessConnection() {

        if (conn != null) {
            conn.disconnect();
        }
        if (process == null) {
            return true;
        }
        if (process.isAlive()) {
            process.destroyForcibly();
            LOG.info("Closing process instance");
        }
        // Let's kill the process error handler
        if (errorHandler.isAlive()) {
            try {
                errorHandler.join();
            } catch (InterruptedException e) {
                LOG.info("Error while trying to close the process error handler", e);
                return false;
            }
        }
        return true;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy