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

com.devonfw.cobigen.api.externalprocess.ExternalProcess Maven / Gradle / Ivy

The newest version!
package com.devonfw.cobigen.api.externalprocess;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.BindException;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.URL;
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.Random;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.zip.GZIPInputStream;

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 org.zeroturnaround.exec.ProcessExecutor;
import org.zeroturnaround.exec.ProcessResult;
import org.zeroturnaround.exec.StartedProcess;
import org.zeroturnaround.exec.stream.slf4j.Slf4jStream;

import com.devonfw.cobigen.api.exception.CobiGenRuntimeException;
import com.devonfw.cobigen.api.externalprocess.constants.ExternalProcessConstants;
import com.devonfw.cobigen.api.util.ExceptionUtil;
import com.google.gson.Gson;

import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

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

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

  /** Property holding the current OS name */
  private static final String OS = System.getProperty("os.name").toLowerCase();

  /** Download URL to get the server from */
  private final String serverDownloadUrl;

  /** Filename of the server executable */
  private final String serverFileName;

  /** Version of the server executable */
  private final String serverVersion;

  /** Port used for connecting to the server */
  private int port = 5000;

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

  /** Context path for all service requests */
  private String contextPath = "";

  /** Native process instance */
  private StartedProcess process;

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

  /** HTTP Client to execute any server call */
  private OkHttpClient httpClient;

  /** HTTP Method enum as OkHttp does not provide an enum */
  public enum HttpMethod {
    GET, POST
  }

  /**
   * Constructor of {@link ExternalProcess}.
   */
  public ExternalProcess(String serverDownloadUrl, String serverFileName, String serverVersion) {

    this(serverDownloadUrl, serverFileName, serverVersion, null, null, null);
  }

  /**
   * Constructor of {@link ExternalProcess}.
   *
   * @param contextPath context path of the API
   */
  public ExternalProcess(String serverDownloadUrl, String serverFileName, String serverVersion, String contextPath) {

    this(serverDownloadUrl, serverFileName, serverVersion, contextPath, null, null);
  }

  /**
   * Constructor of {@link ExternalProcess}.
   *
   * @param contextPath context path of the API
   * @param hostName name of the server, normally localhost
   * @param port port to be used for connecting to the server
   */
  public ExternalProcess(String serverDownloadUrl, String serverFileName, String serverVersion, String contextPath,
      String hostName, Integer port) {

    if (contextPath != null) {
      this.contextPath = contextPath;
    }
    if (hostName != null) {
      this.hostName = hostName;
    }
    if (port != null) {
      this.port = port;
    }
    this.serverDownloadUrl = serverDownloadUrl;
    this.serverVersion = serverVersion;
    this.serverFileName = serverFileName;

    this.httpClient = new OkHttpClient().newBuilder().connectTimeout(10, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS).callTimeout(30, TimeUnit.SECONDS).writeTimeout(30, TimeUnit.SECONDS)
        .retryOnConnectionFailure(true).build();
  }

  /**
   * General request sent to the external server making sure, that the server has been started beforehand.
   *
   * @param httpMethod the http method to send the request with
   * @param path the relative path of the service
   * @param body the body to sent
   * @param mediaType the mediaType with which the body should be sent
   * @return the plain response
   */
  public String request(HttpMethod httpMethod, String path, Object body, MediaType mediaType) {

    startServer();
    return _request(httpMethod, path, body, mediaType);
  }

  /**
   * {@link #request(HttpMethod, String, Object, MediaType)} without requesting the server to start
   */
  @SuppressWarnings("javadoc")
  private String _request(HttpMethod httpMethod, String path, Object body, MediaType mediaType) {

    String endpointUrl = getBasePath() + path;
    LOG.debug("Requesting {} {} with media type {}", httpMethod, endpointUrl, mediaType);
    try {
      Response response = null;
      switch (httpMethod) {
        case POST:
          response = this.httpClient.newCall(new Request.Builder().url(endpointUrl)
              .post(RequestBody.create(new Gson().toJson(body), mediaType)).build()).execute();

          break;
        case GET:
          response = this.httpClient.newCall(new Request.Builder().url(endpointUrl).get().build()).execute();
      }

      if (response != null && (response.code() == 200 || response.code() == 201 || response.code() == 204)) {
        LOG.debug("Responded {}", response.code());
        return response.body().string();
      } else {
        throw new CobiGenRuntimeException("Unable to send or receive the message from the service. Response code: "
            + (response != null ? response.code() : null));
      }
    } catch (IOException e) {
      throw new CobiGenRuntimeException("Unable to send or receive the message from the service", e);
    }
  }

  /**
   * 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
   */
  private boolean shouldNotRetry(int statusCode) {

    switch (statusCode) {

      case HttpURLConnection.HTTP_MOVED_TEMP:
        return false;

      case HttpURLConnection.HTTP_UNAVAILABLE:
        return false;

      default:
        return true;
    }

  }

  /**
   * @see #request(HttpMethod, String, Object, MediaType)
   */
  @SuppressWarnings("javadoc")
  public String post(String path, Object body, MediaType mediaType) {

    return request(HttpMethod.POST, path, body, mediaType);
  }

  /**
   * @see #request(HttpMethod, String, Object, MediaType)
   */
  @SuppressWarnings("javadoc")
  public String postJsonRequest(String path, Object body) {

    return post(path, body, MediaType.get("application/json"));
  }

  /**
   * @see #request(HttpMethod, String, Object, MediaType)
   */
  @SuppressWarnings("javadoc")
  public String get(String path, MediaType mediaType) {

    return request(HttpMethod.GET, path, null, mediaType);
  }

  /**
   * @see #request(HttpMethod, String, Object, MediaType)
   */
  @SuppressWarnings("javadoc")
  public String getJsonRequest(String path) {

    return get(path, MediaType.get("application/json"));
  }

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

    // server ist already running
    if (this.process != null && this.process.getProcess() != null && this.process.getProcess().isAlive()) {
      LOG.debug("Server was already running - {}", this.process.getProcess());
      return true;
    } else {
      LOG.debug("Server was not yet running, starting...");
    }

    String fileName;
    if (OS.indexOf("win") >= 0) {
      fileName = this.serverFileName + "-" + this.serverVersion + ".exe";
    } else {
      fileName = this.serverFileName + "-" + this.serverVersion;
    }
    String filePath = ExternalProcessConstants.EXTERNAL_PROCESS_FOLDER.toString() + File.separator + fileName;

    try {
      if (exeIsNotValid(filePath)) {
        filePath = downloadExecutable(filePath, fileName);
      }
      setPermissions(filePath);
    } catch (IOException e) {
      LOG.error("Unable to download {} to {} and set permissions", filePath, this.serverDownloadUrl, e);
      return false;
    }

    int currentTry = 0;
    while (currentTry < 10) {
      try {
        this.process = new ProcessExecutor().command(filePath, String.valueOf(this.port)).destroyOnExit()
            .redirectError(
                Slf4jStream.of(LoggerFactory.getLogger(getClass().getName() + "." + this.serverFileName)).asError())
            .redirectOutput(
                Slf4jStream.of(LoggerFactory.getLogger(getClass().getName() + "." + this.serverFileName)).asInfo())
            .start();
        Future result = this.process.getFuture();

        int retry = 0;
        do {
          if (result.isDone()) { // if process terminated already, it was failing
            LOG.error("Could not start server in 5s. Closed with output:\n{}", result.get().getOutput());
            this.process.getProcess().destroyForcibly();
            return false;
          }
          Thread.sleep(100);
          retry++;
          LOG.info("Waiting process to be alive for {}s", 100 * retry / 1000d);
        } while (!isConnectedAndValidService() && retry <= 50);

        if (retry > 50) {
          LOG.error("Server could not be started at port {}", this.port);
          return false;
        }

        // add JVM shutdown hook
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
          try {
            LOG.info("Closing {} - {}", this.serverFileName, this.process.getProcess());
            ExternalProcess.this.finalize();
          } catch (Throwable e) {
            LOG.warn("Could not close external process", e);
          }
        }));

        LOG.info("Server started at port {}", this.port);
        return true;
      } catch (Throwable e) {
        BindException bindException = ExceptionUtil.getCause(e, BindException.class);
        ConnectException connectException = ExceptionUtil.getCause(e, ConnectException.class);
        if (bindException != null || connectException != null) {
          try {
            this.process.getProcess().destroyForcibly().waitFor();
          } catch (InterruptedException e1) {
            LOG.error("Interrupted wait for process termination to complete", e1);
          }
          int newPort = aquireNewPort();
          LOG.debug("Port {} already in use, trying port {}", this.port, newPort, e);
          this.port = newPort;
          currentTry++;
        }
        throw new CobiGenRuntimeException("Unable to start the exe/server", e);
      }
    }
    LOG.error("Stopped trying to start the server after 10 retries");
    return false;
  }

  /**
   * Getting a new port to start the server with. The new port will be randomly determined.
   *
   * @return the new port
   */
  private int aquireNewPort() {

    return this.port + new Random().nextInt(100);
  }

  /**
   * 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.exists() || !exeFile.isFile()) {
      LOG.debug("{} is not a file", filePath);
      return true;
    }
    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 (OS.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 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 downloadExecutable(String filePath, String fileName) throws IOException {

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

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

    // Downloading tar file
    File tarFile;
    Path tarPath;
    LOG.info("Downloading server from {} to {}", this.serverDownloadUrl, filePath);
    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 (FileInputStream in = new FileInputStream(tarPath.toString());
          InputStream is = new GZIPInputStream(in);
          TarArchiveInputStream tarInputStream = (TarArchiveInputStream) new ArchiveStreamFactory()
              .createArchiveInputStream("tar", is)) {

        TarArchiveEntry entry;
        while ((entry = tarInputStream.getNextTarEntry()) != null) {
          if (entry.isDirectory()) {
            continue;
          }
          if (entry.getName().contains(this.serverVersion)) {
            // We don't want the directories (src/main/server.exe), we just want to create the
            // file (server.exe)
            File targetFile = new File(parentDirectory, fileName);
            try (FileOutputStream fos = new FileOutputStream(targetFile)) {
              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);
      throw new CobiGenRuntimeException(
          "Enable to extract the external server package. Possibly a corrupt download. Please try again");
    }

    // Remove tar file
    Files.deleteIfExists(tarPath);
    return filePath;

  }

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

    String response;
    try {
      response = _request(HttpMethod.GET, ExternalProcessConstants.IS_CONNECTION_READY, null,
          MediaType.get("text/plain"));
    } catch (CobiGenRuntimeException e) {
      LOG.debug("Server not yet available", e);
      return false;
    }

    if (response.equals(this.serverVersion)) {
      LOG.debug("Established connection to the {} server with correct version {}", this.serverFileName,
          this.serverVersion);
      return true;
    } else if (response.equals("true")) {
      throw new CobiGenRuntimeException("The old version " + this.serverVersion + " of " + this.exeName
          + " is currently deployed. This should not happen as the nestserver is automatically deployed.");
    } else {
      LOG.debug("Established connection to the {} server but got wrong response: {}", this.serverFileName, response);
      return false;
    }
  }

  /**
   * @return the base path including http protocol, hostname, port, and context path the server has been started with.
   */
  public String getBasePath() {

    return "http://" + this.hostName + ":" + this.port + this.contextPath;
  }

  /**
   * @return the {@link OkHttpClient} to send requests with
   */
  public OkHttpClient getHttpClient() {

    return this.httpClient;
  }

  @Override
  protected void finalize() throws Throwable {

    if (this.process != null && this.process.getProcess() != null && this.process.getProcess().isAlive()) {
      LOG.info("Terminating TS Merger External Process {}", this.process.getProcess());
      this.process.getProcess().destroyForcibly();
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy