Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
io.github.tontu89.debugserverlib.ClientHandler Maven / Gradle / Ivy
package io.github.tontu89.debugserverlib;
import com.fasterxml.jackson.core.type.TypeReference;
import com.jayway.jsonpath.JsonPath;
import io.github.tontu89.debugserverlib.config.RemoteDebugServerConfig;
import io.github.tontu89.debugserverlib.filter.requestwrapper.CachedBodyHttpServletRequest;
import io.github.tontu89.debugserverlib.model.FilterRequest;
import io.github.tontu89.debugserverlib.model.FilterRequestMatchPattern;
import io.github.tontu89.debugserverlib.model.HttpRequestInfo;
import io.github.tontu89.debugserverlib.model.HttpResponseInfo;
import io.github.tontu89.debugserverlib.model.MessageRequest;
import io.github.tontu89.debugserverlib.model.MessageResponse;
import io.github.tontu89.debugserverlib.model.ServerClientMessage;
import io.github.tontu89.debugserverlib.utils.Constants;
import io.github.tontu89.debugserverlib.utils.DebugUtils;
import io.github.tontu89.debugserverlib.utils.FileUtils;
import io.github.tontu89.debugserverlib.utils.HttpUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.net.Socket;
import java.net.SocketException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static io.github.tontu89.debugserverlib.utils.Constants.LOG_ERROR_PREFIX;
import static io.github.tontu89.debugserverlib.utils.Constants.MAX_REQUEST_TIME_OUT_MS;
import static io.github.tontu89.debugserverlib.utils.Constants.OBJECT_MAPPER;
import static io.github.tontu89.debugserverlib.utils.Constants.POLL_QUEUE_TIMEOUT_MS;
@Slf4j
public class ClientHandler extends Thread implements AutoCloseable {
public enum Status {RUNNING, STOPPED, NOT_RUNNING}
private final BlockingQueue messageRequestFromClientQueue;
private final BlockingQueue messageResponseFromClientQueue;
private final BlockingQueue messageToClientQueue;
private final DataInputStream dis;
private final DataOutputStream dos;
private final RemoteDebugServerConfig remoteDebugServerConfig;
private final Executor executor;
private final ExecutorService processClientRequestExecutor;
private final FilterRequest debugFilterRequest;
private final Map responseForServerRequest;
private final Map serverRequestId;
private final Socket socket;
private final String clientId;
private boolean stop;
private Future sendMessageToClientFuture;
private Future processClientRequestFuture;
private Future processClientResponseFuture;
private Future heartBeatFuture;
private Status status;
private String clientName;
public ClientHandler(RemoteDebugServerConfig remoteDebugServerConfig, DataInputStream dis, DataOutputStream dos, Socket socket) {
this.socket = socket;
this.dis = dis;
this.dos = dos;
this.status = Status.NOT_RUNNING;
this.debugFilterRequest = new FilterRequest();
this.executor = Executors.newCachedThreadPool();
this.messageRequestFromClientQueue = new LinkedBlockingQueue<>();
this.messageResponseFromClientQueue = new LinkedBlockingQueue<>();
this.messageToClientQueue = new LinkedBlockingQueue<>();
this.responseForServerRequest = new ConcurrentHashMap<>();
this.serverRequestId = new ConcurrentHashMap<>();
this.stop = false;
this.clientId = UUID.randomUUID().toString();
this.remoteDebugServerConfig = remoteDebugServerConfig;
this.processClientRequestExecutor = Executors.newFixedThreadPool(this.remoteDebugServerConfig.getNumberOfThreadPerClient() > 1 ? this.remoteDebugServerConfig.getNumberOfThreadPerClient() : 1);
}
@Override
@SneakyThrows
public void run() {
ServerClientMessage receivedMessage;
try {
startSendMessageToClient();
startProcessClientRequest();
startProcessClientResponse();
startSendHeartBeat();
while (!this.stop && !this.socket.isClosed()) {
try {
this.status = Status.RUNNING;
receivedMessage = DebugUtils.readMessage(this.dis);
if (receivedMessage.getType() == ServerClientMessage.Type.REQUEST) {
this.messageRequestFromClientQueue.add(receivedMessage);
} else if (receivedMessage.getType() == ServerClientMessage.Type.RESPONSE) {
this.messageResponseFromClientQueue.add(receivedMessage);
} else {
log.error("DebugLib: Unsupported message type {}", receivedMessage);
}
} catch (EOFException e) {
this.stop = true;
} catch (SocketException e) {
this.stop = true;
log.error(LOG_ERROR_PREFIX + e.getMessage(), e);
} catch (IOException e) {
log.error(LOG_ERROR_PREFIX + e.getMessage(), e);
}
}
} catch (Throwable e) {
log.error(LOG_ERROR_PREFIX + e.getMessage(), e);
}
this.close();
}
public HttpResponseInfo forwardHttpRequestToClient(CachedBodyHttpServletRequest httpRequest, int timeOutInMs) throws Exception {
HttpRequestInfo requestInfo = HttpRequestInfo.fromHttpRequest(httpRequest, true);
MessageRequest messageRequest = MessageRequest.builder()
.command(MessageRequest.Command.CLIENT_EXECUTE_HTTP_REQUEST)
.dataBase64(DebugUtils.objectToBase64String(requestInfo))
.build();
String responseData = this.sendMessageToClient(messageRequest, timeOutInMs);
return DebugUtils.base64StringToObject(responseData, HttpResponseInfo.class);
}
public HttpResponseInfo forwardHttpRequestToClient(CachedBodyHttpServletRequest httpRequest) throws Exception {
return this.forwardHttpRequestToClient(httpRequest, MAX_REQUEST_TIME_OUT_MS);
}
private void startSendMessageToClient() {
this.sendMessageToClientFuture = CompletableFuture.runAsync(() -> {
try {
while (!this.stop || !this.messageToClientQueue.isEmpty()) {
ServerClientMessage message = pollMessageFromQueue(this.messageToClientQueue);
if (message == null) continue;
DebugUtils.writeMessage(this.dos, message);
}
} catch (Throwable e) {
log.error(LOG_ERROR_PREFIX + e.getMessage(), e);
this.closeAsync();
}
}, this.executor);
}
private void startProcessClientRequest() {
this.processClientRequestFuture = CompletableFuture.runAsync(() -> {
while (!this.stop) {
ServerClientMessage message = pollMessageFromQueue(this.messageRequestFromClientQueue);
if (message == null) continue;
CompletableFuture.runAsync(() -> {
MessageRequest messageRequest = message.getRequest();
MessageResponse messageResponse = MessageResponse.builder()
.status(HttpStatus.OK.value())
.build();
try {
switch (messageRequest.getCommand()) {
case SERVER_EXIT:
this.stop = true;
break;
case SERVER_GET_ENV:
messageResponse.encodeDataBase64(System.getenv());
break;
case SERVER_GET_PROP:
messageResponse.encodeDataBase64(System.getProperties());
break;
case SERVER_ADD_FILTER_PATTERN:
List matchPatterns = DebugUtils.base64StringToObject(messageRequest.getDataBase64(), new TypeReference>() {
});
matchPatterns.forEach(e -> e.init());
this.debugFilterRequest.addPattern(matchPatterns);
break;
case SERVER_GET_ALL_FILTER_PATTERN:
messageResponse.encodeDataBase64(this.debugFilterRequest.getMatchPatterns());
break;
case SERVER_CLEAR_ALL_FILTER_PATTERN:
this.debugFilterRequest.getMatchPatterns().clear();
break;
case SERVER_EXECUTE_HTTP_REQUEST:
HttpRequestInfo clientRequestInfo = DebugUtils.base64StringToObject(messageRequest.getDataBase64(), HttpRequestInfo.class);
messageResponse.encodeDataBase64(this.executeClientHttpRequest(clientRequestInfo));
break;
case SERVER_DOWNLOAD_FILE:
String filePath = DebugUtils.base64StringToObject(messageRequest.getDataBase64(), String.class);
messageResponse = FileUtils.downloadFile(filePath);
break;
case SERVER_SET_CLIENT_NAME:
this.clientName = DebugUtils.base64StringToObject(messageRequest.getDataBase64(), String.class);
break;
case HEART_BEAT:
messageResponse.setStatus(Constants.HEART_BEAT_RESPONSE_CODE);
break;
}
} catch (Throwable e) {
log.error(LOG_ERROR_PREFIX + e.getMessage(), e);
messageResponse.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
}
ServerClientMessage serverClientMessage = ServerClientMessage.builder()
.id(message.getId())
.type(ServerClientMessage.Type.RESPONSE)
.response(messageResponse)
.build();
this.messageToClientQueue.add(serverClientMessage);
}, this.processClientRequestExecutor);
}
}, this.executor);
}
private void startProcessClientResponse() {
this.processClientResponseFuture = CompletableFuture.runAsync(() -> {
try {
while (!this.stop || !this.messageResponseFromClientQueue.isEmpty()) {
ServerClientMessage message = pollMessageFromQueue(this.messageResponseFromClientQueue);
if (message == null) continue;
try {
String messageId = this.serverRequestId.get(message.getId());
this.responseForServerRequest.put(messageId, message);
synchronized (messageId) {
messageId.notifyAll();
}
} catch (Throwable e) {
log.error(LOG_ERROR_PREFIX + e.getMessage(), e);
}
}
} catch (Throwable e) {
log.error(LOG_ERROR_PREFIX + e.getMessage(), e);
this.closeAsync();
}
}, this.executor);
}
public boolean isMatch(CachedBodyHttpServletRequest httpRequest) {
if (this.isRunning()) {
try {
String httpRequestJsonFormat = OBJECT_MAPPER.writeValueAsString(HttpRequestInfo.fromHttpRequest(httpRequest, true));
log.debug("DebugLib: Matching httpRequestJsonFormat [{}]", httpRequestJsonFormat);
return this.debugFilterRequest.isMatch(JsonPath.parse(httpRequestJsonFormat));
} catch (Throwable e) {
log.error(LOG_ERROR_PREFIX + e.getMessage(), e);
}
}
return false;
}
public String getClientId() {
return this.clientId;
}
public String getClientName() {
return this.clientName;
}
@Override
public void close() {
this.stop = true;
this.status = Status.STOPPED;
try {
log.info("DebugLib: Notify all watcher before closing client connection");
synchronized (this) {
this.notifyAll();
}
} catch (Throwable e) {
log.error(LOG_ERROR_PREFIX + e.getMessage(), e);
}
log.info("DebugLib: Client " + this.socket + " sends exit...");
log.info("DebugLib: Closing this connection.");
log.info("DebugLib: Connection closed");
Arrays.asList(this.processClientRequestFuture, this.sendMessageToClientFuture, this.processClientResponseFuture, this.heartBeatFuture).forEach((f) -> {
try {
if (f != null && !f.isDone()) {
f.get();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Throwable e) {
log.error(LOG_ERROR_PREFIX + e.getMessage(), e);
}
});
try {
this.processClientRequestExecutor.shutdownNow();
} catch (Throwable e) {
log.error("DebugLib: Error happen when shutdown process client executor service" + e.getMessage(), e);
}
try {
this.processClientRequestExecutor.awaitTermination(1, TimeUnit.MINUTES);
} catch (Throwable e) {
log.error("DebugLib: Timeout when shutdown process client executor service" + e.getMessage(), e);
}
this.serverRequestId.forEach((key, value) -> {
try {
synchronized (value) {
value.notifyAll();
}
} catch (Throwable e) {
log.error("DebugLib: Error happen with ({}, {}) when notify object for closing", key, value);
log.error(LOG_ERROR_PREFIX + e.getMessage(), e);
}
});
// closing resources
if (this.dis != null) {
try {
this.dis.close();
} catch (IOException e) {
log.error(LOG_ERROR_PREFIX + e.getMessage(), e);
}
}
if (this.dos != null) {
try {
this.dos.close();
} catch (IOException e) {
log.error(LOG_ERROR_PREFIX + e.getMessage(), e);
}
}
if (this.socket != null && !this.socket.isClosed()) {
try {
this.socket.close();
} catch (IOException e) {
log.error(LOG_ERROR_PREFIX + e.getMessage(), e);
}
}
}
public boolean isRunning() {
return this.status == Status.RUNNING;
}
public Status getStatus() {
return this.status;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ClientHandler that = (ClientHandler) o;
return clientId.equals(that.clientId);
}
@Override
public int hashCode() {
return clientId.hashCode();
}
private ServerClientMessage pollMessageFromQueue(BlockingQueue queue) {
try {
return queue.poll(POLL_QUEUE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
return null;
}
private HttpResponseInfo executeClientHttpRequest(HttpRequestInfo httpRequestInfo) {
HttpResponseInfo responseInfo = HttpUtils.executeHttpRequestByRest(
httpRequestInfo.getUri(),
null,
httpRequestInfo.getMethod(),
httpRequestInfo.getHeaders(),
httpRequestInfo.getPayload()
);
return responseInfo;
}
private void closeAsync() {
CompletableFuture.runAsync(() -> this.close(), this.executor);
}
private void startSendHeartBeat() {
if (this.remoteDebugServerConfig.isEnableHeartBeat()) {
this.heartBeatFuture = CompletableFuture.runAsync(() -> {
try {
log.info("DebugLib: Heart beat is running");
MessageRequest messageRequest = MessageRequest.builder()
.command(MessageRequest.Command.HEART_BEAT)
.build();
while (!this.stop) {
try {
this.sendMessageToClient(messageRequest, this.remoteDebugServerConfig.getHeartBeatTimeoutMs());
Thread.sleep(this.remoteDebugServerConfig.getHeartBeatIntervalMs());
} catch (Throwable e) {
log.error(LOG_ERROR_PREFIX + " HeartBeat check exception: " + e.getMessage(), e);
break;
}
}
log.info("DebugLib: Heart beat is stopped");
} catch (Throwable e) {
log.error(LOG_ERROR_PREFIX + e.getMessage(), e);
}
this.closeAsync();
}, this.executor);
}
}
private String sendMessageToClient(MessageRequest messageRequest, int timeOutInMs) throws Exception {
String messageId = "SERVER-" + UUID.randomUUID();
this.serverRequestId.put(messageId, messageId);
ServerClientMessage serverClientMessage = ServerClientMessage.builder()
.id(messageId)
.type(ServerClientMessage.Type.REQUEST)
.request(messageRequest)
.build();
this.messageToClientQueue.add(serverClientMessage);
if (messageRequest.getCommand() != MessageRequest.Command.HEART_BEAT) {
log.debug("DebugLib: Sending message to client {}", serverClientMessage);
}
synchronized (messageId) {
try {
if (messageRequest.getCommand() != MessageRequest.Command.HEART_BEAT) {
log.info("DebugLib: Server request {}: Wait response", messageId);
}
int i = timeOutInMs / 500;
while (!this.stop && !this.responseForServerRequest.containsKey(messageId) && i-- > 0)
messageId.wait(500);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
if (!this.responseForServerRequest.containsKey(messageId)) {
log.error("DebugLib: Server request {}: timeout after {} minutes for request [{}]", messageId, Math.round((timeOutInMs / 1000.0 / 60.0) * 100.0) / 100.0, messageRequest);
throw new TimeoutException("Timeout after " + timeOutInMs + " ms");
} else {
ServerClientMessage message = this.responseForServerRequest.get(messageId);
if (messageRequest.getCommand() != MessageRequest.Command.HEART_BEAT) {
log.info("DebugLib: Server request {}: Received response with: {}", messageId, message);
}
if (!messageId.equals(message.getId())) {
throw new Exception("Unexpected error");
}
return message.getResponse().getDataBase64();
}
}
}
}