com.devonfw.cobigen.api.externalprocess.ExternalProcess Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of core-externalprocess-api Show documentation
Show all versions of core-externalprocess-api Show documentation
A Code-based incremental Generator
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