694 lines
23 KiB
Java
694 lines
23 KiB
Java
package li.strolch.plc.gw.client;
|
|
|
|
import static java.net.NetworkInterface.getByInetAddress;
|
|
import static li.strolch.model.Tags.Json.*;
|
|
import static li.strolch.plc.model.ModelHelper.valueToJson;
|
|
import static li.strolch.plc.model.PlcConstants.*;
|
|
import static li.strolch.runtime.StrolchConstants.DEFAULT_REALM;
|
|
import static li.strolch.utils.helper.ExceptionHelper.*;
|
|
import static li.strolch.utils.helper.NetworkHelper.formatMacAddress;
|
|
import static li.strolch.utils.helper.StringHelper.isEmpty;
|
|
|
|
import java.io.IOException;
|
|
import java.net.SocketException;
|
|
import java.net.URI;
|
|
import java.nio.ByteBuffer;
|
|
import java.util.Collections;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.Map;
|
|
import java.util.concurrent.Future;
|
|
import java.util.concurrent.LinkedBlockingDeque;
|
|
import java.util.concurrent.ScheduledFuture;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
import com.google.gson.JsonArray;
|
|
import com.google.gson.JsonObject;
|
|
import com.google.gson.JsonParser;
|
|
import jakarta.websocket.*;
|
|
import jakarta.websocket.CloseReason.CloseCodes;
|
|
import li.strolch.agent.api.*;
|
|
import li.strolch.model.Locator;
|
|
import li.strolch.model.Resource;
|
|
import li.strolch.model.log.LogMessage;
|
|
import li.strolch.model.parameter.StringParameter;
|
|
import li.strolch.persistence.api.StrolchTransaction;
|
|
import li.strolch.plc.core.GlobalPlcListener;
|
|
import li.strolch.plc.core.PlcHandler;
|
|
import li.strolch.plc.model.*;
|
|
import li.strolch.privilege.model.PrivilegeContext;
|
|
import li.strolch.runtime.configuration.ComponentConfiguration;
|
|
import li.strolch.utils.CheckedRunnable;
|
|
import li.strolch.utils.helper.NetworkHelper;
|
|
import org.glassfish.tyrus.client.ClientManager;
|
|
|
|
public class PlcGwClientHandler extends StrolchComponent implements GlobalPlcListener {
|
|
|
|
public static final String PLC = "PLC";
|
|
public static final String SERVER_CONNECTED = "ServerConnected";
|
|
public static final PlcAddressKey K_PLC_SERVER_CONNECTED = PlcAddressKey.keyFor(PLC, SERVER_CONNECTED);
|
|
|
|
private static final long PING_DELAY = 90;
|
|
private static final long RETRY_DELAY = 30;
|
|
private static final int INITIAL_DELAY = 10;
|
|
|
|
private boolean verbose;
|
|
|
|
private String plcId;
|
|
private boolean gwConnectToServer;
|
|
private String gwUsername;
|
|
private String gwPassword;
|
|
private String gwServerUrl;
|
|
|
|
private PlcHandler plcHandler;
|
|
|
|
private ClientManager gwClient;
|
|
private volatile Session gwSession;
|
|
private volatile boolean authenticated;
|
|
|
|
private ScheduledFuture<?> serverConnectFuture;
|
|
|
|
private Map<PlcAddress, Object> notConnectedQueue;
|
|
private LinkedBlockingDeque<CheckedRunnable> messageQueue;
|
|
private int maxMessageQueue;
|
|
private boolean run;
|
|
private Future<?> messageSenderTask;
|
|
|
|
private long lastSystemStateNotification;
|
|
private long ipAddressesUpdateTime;
|
|
private JsonArray ipAddresses;
|
|
private JsonObject versions;
|
|
|
|
public PlcGwClientHandler(ComponentContainer container, String componentName) {
|
|
super(container, componentName);
|
|
}
|
|
|
|
@Override
|
|
public void initialize(ComponentConfiguration configuration) throws Exception {
|
|
|
|
this.verbose = configuration.getBoolean("verbose", false);
|
|
this.plcId = getComponent(PlcHandler.class).getPlcId();
|
|
this.gwConnectToServer = configuration.getBoolean("gwConnectToServer", true);
|
|
this.gwUsername = configuration.getString("gwUsername", null);
|
|
this.gwPassword = configuration.getString("gwPassword", null);
|
|
this.gwServerUrl = configuration.getString("gwServerUrl", null);
|
|
|
|
this.maxMessageQueue = configuration.getInt("maxMessageQueue", 100);
|
|
this.messageQueue = new LinkedBlockingDeque<>();
|
|
this.notConnectedQueue = Collections.synchronizedMap(new LinkedHashMap<>());
|
|
|
|
super.initialize(configuration);
|
|
}
|
|
|
|
@Override
|
|
public void start() throws Exception {
|
|
|
|
this.plcHandler = getComponent(PlcHandler.class);
|
|
|
|
if (this.plcHandler.getPlcState() == PlcState.Started)
|
|
notifyPlcConnectionState(ConnectionState.Disconnected);
|
|
this.plcHandler.setGlobalListener(this);
|
|
|
|
if (this.gwConnectToServer) {
|
|
delayConnect(INITIAL_DELAY, TimeUnit.SECONDS);
|
|
this.run = true;
|
|
this.messageSenderTask = getExecutorService("MessageSender").submit(this::sendMessages);
|
|
}
|
|
|
|
super.start();
|
|
}
|
|
|
|
private void notifyPlcConnectionState(ConnectionState disconnected) {
|
|
getExecutorService("MessageSender").submit(() -> {
|
|
try {
|
|
getComponent(PlcHandler.class).notify(PLC, SERVER_CONNECTED, disconnected.name());
|
|
} catch (Exception e) {
|
|
logger.error("Failed to notify PLC of connection state", e);
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void stop() throws Exception {
|
|
|
|
this.run = false;
|
|
this.authenticated = false;
|
|
if (this.messageSenderTask != null)
|
|
this.messageSenderTask.cancel(true);
|
|
|
|
notifyPlcConnectionState(ConnectionState.Disconnected);
|
|
|
|
if (this.gwSession != null) {
|
|
try {
|
|
this.gwSession.close(new CloseReason(CloseCodes.GOING_AWAY, "Shutting down"));
|
|
} catch (Exception e) {
|
|
logger.error("Failed to close server session: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
if (this.gwClient != null) {
|
|
this.gwClient.shutdown();
|
|
}
|
|
|
|
if (this.serverConnectFuture != null) {
|
|
this.serverConnectFuture.cancel(true);
|
|
}
|
|
|
|
super.stop();
|
|
}
|
|
|
|
private void delayConnect(long delay, TimeUnit unit) {
|
|
if (this.serverConnectFuture != null)
|
|
this.serverConnectFuture.cancel(true);
|
|
this.serverConnectFuture = getScheduledExecutor("ConnectionTimer").schedule(this::connectToServer, delay, unit);
|
|
}
|
|
|
|
private void connectToServer() {
|
|
|
|
// connect to Server
|
|
logger.info("Connecting to Server at " + this.gwServerUrl + "...");
|
|
try {
|
|
this.gwClient = ClientManager.createClient();
|
|
this.gwSession = this.gwClient.connectToServer(new PlcGwClientEndpoint(this), new URI(this.gwServerUrl));
|
|
} catch (Exception e) {
|
|
Throwable rootCause = getRootCause(e);
|
|
if (rootCause instanceof InterruptedException) {
|
|
logger.error("Interrupted while connecting. Stopping.");
|
|
return;
|
|
}
|
|
|
|
if (rootCause.getMessage() != null && rootCause.getMessage().contains("Connection refused")) {
|
|
logger.error(
|
|
"Connection refused to connect to server. Will try to connect again in " + RETRY_DELAY + "s: "
|
|
+ getExceptionMessageWithCauses(e));
|
|
} else if (rootCause.getMessage() != null && rootCause.getMessage()
|
|
.contains("Response code was not 101: 404.")) {
|
|
logger.error("Connection failed with 404 error code. Is URL " + this.gwServerUrl + " correct?");
|
|
logger.error("Server not yet ready with 404 error. Will try again in " + RETRY_DELAY + "s");
|
|
} else {
|
|
logger.error("Failed to connect to server! Will try to connect again in " + RETRY_DELAY + "s", e);
|
|
}
|
|
|
|
closeBrokenGwSessionUpdateState("Failed to connect to server",
|
|
"Connection refused to connect to server. Will try to connect again in " + RETRY_DELAY + "s: "
|
|
+ getExceptionMessageWithCauses(e));
|
|
delayConnect(RETRY_DELAY, TimeUnit.SECONDS);
|
|
return;
|
|
}
|
|
|
|
// register session on server by sending a heart beat, which sends our plcId
|
|
this.lastSystemStateNotification = System.currentTimeMillis();
|
|
if (tryPingServer()) {
|
|
logger.error("Failed to ping server. Will try to connect again in " + RETRY_DELAY + "s");
|
|
closeBrokenGwSessionUpdateState("Ping failed",
|
|
"Failed to ping server. Will try to connect again in " + RETRY_DELAY + "s");
|
|
delayConnect(RETRY_DELAY, TimeUnit.SECONDS);
|
|
return;
|
|
}
|
|
|
|
// now authenticate
|
|
JsonObject authJ = new JsonObject();
|
|
authJ.addProperty(PARAM_MESSAGE_TYPE, MSG_TYPE_AUTHENTICATION);
|
|
authJ.addProperty(PARAM_PLC_ID, this.plcId);
|
|
authJ.addProperty(PARAM_USERNAME, this.gwUsername);
|
|
authJ.addProperty(PARAM_PASSWORD, this.gwPassword);
|
|
authJ.add(PARAM_IP_ADDRESSES, getIpAddresses());
|
|
authJ.add(PARAM_VERSIONS, getVersions());
|
|
authJ.add(PARAM_SYSTEM_STATE, getContainer().getAgent().getSystemState(1, TimeUnit.HOURS));
|
|
try {
|
|
sendDataToClient(authJ);
|
|
} catch (IOException e) {
|
|
String msg = "Failed to send Auth to server";
|
|
logger.error(msg, e);
|
|
closeBrokenGwSessionUpdateState(msg, msg);
|
|
delayConnect(RETRY_DELAY, TimeUnit.SECONDS);
|
|
return;
|
|
}
|
|
|
|
logger.info(this.gwSession.getId() + ": Connected to Server.");
|
|
|
|
// schedule the heart beat timer
|
|
if (this.serverConnectFuture != null)
|
|
this.serverConnectFuture.cancel(true);
|
|
this.serverConnectFuture = getScheduledExecutor("Server").scheduleWithFixedDelay(this::pingServer, PING_DELAY,
|
|
PING_DELAY, TimeUnit.SECONDS);
|
|
}
|
|
|
|
private void closeBrokenGwSessionUpdateState(String closeReason, String connectionStateMsg) {
|
|
try {
|
|
runAsAgent(ctx -> closeBrokenGwSessionUpdateState(ctx, closeReason, connectionStateMsg));
|
|
} catch (Exception e) {
|
|
logger.error("Failed to close GW Session!", e);
|
|
}
|
|
|
|
notifyPlcConnectionState(ConnectionState.Failed);
|
|
}
|
|
|
|
private void closeBrokenGwSessionUpdateState(PrivilegeContext ctx, String closeReason, String connectionStateMsg) {
|
|
saveServerConnectionState(ctx, ConnectionState.Failed, connectionStateMsg);
|
|
closeGwSession(closeReason);
|
|
}
|
|
|
|
private void closeGwSession(String msg) {
|
|
logger.info("Closing GW session: " + msg);
|
|
this.authenticated = false;
|
|
|
|
if (this.serverConnectFuture != null)
|
|
this.serverConnectFuture.cancel(true);
|
|
|
|
if (this.gwSession != null && this.gwSession.isOpen()) {
|
|
try {
|
|
this.gwSession.close(new CloseReason(CloseCodes.UNEXPECTED_CONDITION, msg));
|
|
} catch (Exception e) {
|
|
logger.error("Failed to close server session due to " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
if (this.gwClient != null) {
|
|
this.gwClient.shutdown();
|
|
}
|
|
|
|
this.gwClient = null;
|
|
this.gwSession = null;
|
|
}
|
|
|
|
private void pingServer() {
|
|
if (tryPingServer()) {
|
|
logger.error("Failed to ping server. Reconnecting...");
|
|
closeBrokenGwSessionUpdateState("Ping failed",
|
|
"Failed to ping server. Will try to connect again in " + RETRY_DELAY + "s");
|
|
delayConnect(RETRY_DELAY, TimeUnit.MILLISECONDS);
|
|
}
|
|
}
|
|
|
|
private boolean tryPingServer() {
|
|
try {
|
|
|
|
logger.info(this.gwSession.getId() + ": Pinging Server...");
|
|
this.gwSession.getBasicRemote().sendPong(ByteBuffer.wrap(this.plcId.getBytes()));
|
|
|
|
long lastUpdate = System.currentTimeMillis() - this.lastSystemStateNotification;
|
|
if (lastUpdate > TimeUnit.HOURS.toMillis(1)) {
|
|
logger.info("Sending system state to server...");
|
|
JsonObject stateJ = new JsonObject();
|
|
stateJ.addProperty(PARAM_MESSAGE_TYPE, MSG_TYPE_STATE_NOTIFICATION);
|
|
stateJ.addProperty(PARAM_PLC_ID, this.plcId);
|
|
stateJ.add(PARAM_IP_ADDRESSES, getIpAddresses());
|
|
stateJ.add(PARAM_VERSIONS, getVersions());
|
|
stateJ.add(PARAM_SYSTEM_STATE, getContainer().getAgent().getSystemState(1, TimeUnit.HOURS));
|
|
|
|
sendDataToClient(stateJ);
|
|
this.lastSystemStateNotification = System.currentTimeMillis();
|
|
}
|
|
|
|
return false;
|
|
|
|
} catch (Exception e) {
|
|
logger.error("Failed to send Ping to Server, closing server session due to: " + getExceptionMessage(e));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private void async(CheckedRunnable runnable) {
|
|
if (this.messageQueue.size() > this.maxMessageQueue)
|
|
this.messageQueue.removeFirst();
|
|
this.messageQueue.addLast(runnable);
|
|
}
|
|
|
|
@Override
|
|
public void sendMsg(LogMessage message) {
|
|
async(() -> {
|
|
|
|
JsonObject messageJ = new JsonObject();
|
|
messageJ.addProperty(PARAM_PLC_ID, this.plcId);
|
|
messageJ.addProperty(PARAM_MESSAGE_TYPE, MSG_TYPE_MESSAGE);
|
|
messageJ.add(PARAM_MESSAGE, message.toJson());
|
|
|
|
sendDataToClient(messageJ);
|
|
if (this.verbose)
|
|
logger.info("Sent msg " + message.getLocator() + " to server");
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void disableMsg(Locator locator) {
|
|
async(() -> {
|
|
|
|
JsonObject messageJ = new JsonObject();
|
|
messageJ.addProperty(PARAM_PLC_ID, this.plcId);
|
|
messageJ.addProperty(PARAM_MESSAGE_TYPE, MSG_TYPE_DISABLE_MESSAGE);
|
|
messageJ.addProperty(PARAM_REALM, DEFAULT_REALM);
|
|
messageJ.addProperty(PARAM_LOCATOR, locator.toString());
|
|
|
|
sendDataToClient(messageJ);
|
|
if (this.verbose)
|
|
logger.info("Sent msg " + locator + " to server");
|
|
});
|
|
}
|
|
|
|
private void notifyServer(PlcAddress plcAddress, Object value) {
|
|
if (!plcAddress.remote)
|
|
return;
|
|
|
|
async(() -> {
|
|
|
|
JsonObject notificationJ = new JsonObject();
|
|
notificationJ.addProperty(PARAM_PLC_ID, this.plcId);
|
|
notificationJ.addProperty(PARAM_MESSAGE_TYPE, MSG_TYPE_PLC_NOTIFICATION);
|
|
notificationJ.addProperty(PARAM_RESOURCE, plcAddress.resource);
|
|
notificationJ.addProperty(PARAM_ACTION, plcAddress.action);
|
|
notificationJ.add(PARAM_VALUE, valueToJson(value));
|
|
|
|
sendDataToClient(notificationJ);
|
|
|
|
if (this.verbose)
|
|
logger.info("Sent notification for " + plcAddress.toKey() + " to server");
|
|
});
|
|
}
|
|
|
|
public void onWsMessage(String message) {
|
|
JsonObject jsonObject = JsonParser.parseString(message).getAsJsonObject();
|
|
if (!jsonObject.has(PARAM_MESSAGE_TYPE)) {
|
|
logger.error("Received data has no message type!");
|
|
return;
|
|
}
|
|
|
|
String messageType = jsonObject.get(PARAM_MESSAGE_TYPE).getAsString();
|
|
|
|
try {
|
|
runAsAgent(ctx -> {
|
|
if (MSG_TYPE_AUTHENTICATION.equals(messageType)) {
|
|
handleAuthResponse(ctx, jsonObject);
|
|
} else if (MSG_TYPE_PLC_TELEGRAM.equals(messageType)) {
|
|
async(() -> handleTelegram(jsonObject));
|
|
} else if (MSG_TYPE_PLC_GET_ADDRESS_STATE.equals(messageType)) {
|
|
async(() -> handleGetAddressState(ctx, jsonObject));
|
|
} else {
|
|
logger.error("Unhandled message type " + messageType);
|
|
}
|
|
});
|
|
} catch (Exception e) {
|
|
throw new IllegalStateException("Failed to handle message of type " + messageType);
|
|
}
|
|
}
|
|
|
|
private void handleGetAddressState(PrivilegeContext ctx, JsonObject telegramJ) throws Exception {
|
|
PlcAddress plcAddress = null;
|
|
try (StrolchTransaction tx = openTx(ctx.getCertificate(), true)) {
|
|
plcAddress = parsePlcAddress(telegramJ);
|
|
|
|
String plcAddressId = this.plcHandler.getPlcAddressId(plcAddress.resource, plcAddress.action);
|
|
Resource address = tx.getResourceBy(TYPE_PLC_ADDRESS, plcAddressId, true);
|
|
Object value = address.getParameter(PARAM_VALUE, true).getValue();
|
|
telegramJ.add(PARAM_VALUE, valueToJson(value));
|
|
|
|
telegramJ.addProperty(PARAM_STATE, PlcResponseState.Done.name());
|
|
telegramJ.addProperty(PARAM_STATE_MSG, "");
|
|
|
|
if (this.verbose)
|
|
logger.info("Sent address state for " + plcAddress.toKey() + " = " + value + " to server");
|
|
|
|
} catch (Exception e) {
|
|
handleFailedTelegram(telegramJ, plcAddress, e);
|
|
}
|
|
|
|
sendDataToClient(telegramJ);
|
|
}
|
|
|
|
private void handleTelegram(JsonObject telegramJ) throws Exception {
|
|
|
|
PlcAddress plcAddress = null;
|
|
try {
|
|
plcAddress = parsePlcAddress(telegramJ);
|
|
|
|
if (telegramJ.has(PARAM_VALUE)) {
|
|
String valueS = telegramJ.get(PARAM_VALUE).getAsString();
|
|
Object value = plcAddress.valueType.parseValue(valueS);
|
|
this.plcHandler.send(plcAddress.resource, plcAddress.action, value, false, false);
|
|
} else {
|
|
this.plcHandler.send(plcAddress.resource, plcAddress.action, false, false);
|
|
}
|
|
|
|
telegramJ.addProperty(PARAM_STATE, PlcResponseState.Done.name());
|
|
telegramJ.addProperty(PARAM_STATE_MSG, "");
|
|
|
|
} catch (Exception e) {
|
|
handleFailedTelegram(telegramJ, plcAddress, e);
|
|
}
|
|
|
|
sendDataToClient(telegramJ);
|
|
|
|
if (this.verbose)
|
|
logger.info("Sent Telegram response for " + (plcAddress == null ? "unknown" : plcAddress.toKey())
|
|
+ " to server");
|
|
}
|
|
|
|
private void handleAuthResponse(PrivilegeContext ctx, JsonObject response) {
|
|
|
|
if (!response.has(PARAM_STATE) || !response.has(PARAM_STATE_MSG) || !response.has(PARAM_AUTH_TOKEN)) {
|
|
|
|
closeBrokenGwSessionUpdateState(ctx, "Auth failed!",
|
|
"Failed to authenticated with Server: At least one of " + PARAM_STATE + ", " + PARAM_STATE_MSG
|
|
+ ", " + PARAM_AUTH_TOKEN + " params is missing on Auth Response");
|
|
|
|
throw new IllegalStateException(
|
|
"Failed to authenticated with Server: At least one of " + PARAM_STATE + ", " + PARAM_STATE_MSG
|
|
+ ", " + PARAM_AUTH_TOKEN + " params is missing on Auth Response");
|
|
}
|
|
|
|
if (PlcResponseState.valueOf(response.get(PARAM_STATE).getAsString()) != PlcResponseState.Sent) {
|
|
closeBrokenGwSessionUpdateState(ctx, "Failed to authenticated with server!",
|
|
"Failed to authenticated with Server: " + response.get(PARAM_STATE_MSG).getAsString());
|
|
throw new IllegalStateException("Auth failed to Server: " + response.get(PARAM_STATE_MSG).getAsString());
|
|
}
|
|
|
|
String serverAuthToken = response.get(PARAM_AUTH_TOKEN).getAsString();
|
|
if (isEmpty(serverAuthToken)) {
|
|
closeBrokenGwSessionUpdateState(ctx, "Missing auth token on AUTH response!",
|
|
"Missing auth token on AUTH response!");
|
|
throw new IllegalStateException("Missing auth token on AUTH response!");
|
|
}
|
|
logger.info(this.gwSession.getId() + ": Successfully authenticated with Server!");
|
|
|
|
saveServerConnectionState(ctx, ConnectionState.Connected, "");
|
|
notifyPlcConnectionState(ConnectionState.Connected);
|
|
this.authenticated = true;
|
|
|
|
// we are connected, so flush messages
|
|
//noinspection SynchronizeOnNonFinalField
|
|
synchronized (this.notConnectedQueue) {
|
|
this.notConnectedQueue.forEach(this::notifyServer);
|
|
this.notConnectedQueue.clear();
|
|
}
|
|
}
|
|
|
|
public void onWsPong(PongMessage message, Session session) {
|
|
logger.info(session.getId() + ": Received pong " + message.toString());
|
|
}
|
|
|
|
public void onWsOpen(Session session) {
|
|
logger.info(session.getId() + ": New Session");
|
|
}
|
|
|
|
public void onWsClose(Session session, CloseReason closeReason) {
|
|
this.authenticated = false;
|
|
logger.info("Session closed with ID " + session.getId() + " due to " + closeReason.getCloseCode() + " "
|
|
+ closeReason.getReasonPhrase() + ". Reconnecting in " + RETRY_DELAY + "s.");
|
|
|
|
if (this.gwSession != null) {
|
|
closeBrokenGwSessionUpdateState(closeReason.getReasonPhrase(),
|
|
"Session closed with ID " + session.getId() + " due to " + closeReason.getCloseCode() + " "
|
|
+ closeReason.getReasonPhrase() + ". Reconnecting in " + RETRY_DELAY + "s.");
|
|
}
|
|
|
|
delayConnect(RETRY_DELAY, TimeUnit.SECONDS);
|
|
}
|
|
|
|
public void onWsError(Session session, Throwable throwable) {
|
|
logger.error(session.getId() + ": Received error: " + throwable.getMessage(), throwable);
|
|
}
|
|
|
|
@SuppressWarnings("SynchronizeOnNonFinalField")
|
|
private void sendDataToClient(JsonObject jsonObject) throws IOException {
|
|
if (this.gwSession == null)
|
|
throw new IOException("gwSession null! Not authenticated!");
|
|
String data = jsonObject.toString();
|
|
synchronized (this.gwSession) {
|
|
RemoteEndpoint.Basic basic = this.gwSession.getBasicRemote();
|
|
int pos = 0;
|
|
while (pos + 8192 < data.length()) {
|
|
basic.sendText(data.substring(pos, pos + 8192), false);
|
|
pos += 8192;
|
|
}
|
|
basic.sendText(data.substring(pos), true);
|
|
}
|
|
}
|
|
|
|
private void sendMessages() {
|
|
while (this.run) {
|
|
|
|
if (!this.authenticated) {
|
|
try {
|
|
Thread.sleep(100L);
|
|
} catch (InterruptedException e) {
|
|
logger.error("Interrupted!");
|
|
if (!this.run)
|
|
return;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
CheckedRunnable runnable = null;
|
|
try {
|
|
runnable = this.messageQueue.takeFirst();
|
|
runnable.run();
|
|
} catch (Exception e) {
|
|
closeBrokenGwSessionUpdateState("Failed to send message",
|
|
"Failed to send message, reconnecting in " + RETRY_DELAY + "s.");
|
|
if (runnable != null) {
|
|
this.messageQueue.addFirst(runnable);
|
|
logger.error(
|
|
"Failed to send message, reconnecting in " + RETRY_DELAY + "s. And then retrying message.",
|
|
e);
|
|
}
|
|
|
|
delayConnect(RETRY_DELAY, TimeUnit.SECONDS);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void saveServerConnectionState(PrivilegeContext ctx, ConnectionState state, String stateMsg) {
|
|
|
|
StrolchRealm realm = getContainer().getRealm(ctx.getCertificate());
|
|
try (StrolchTransaction tx = realm.openTx(ctx.getCertificate(), "saveServerConnectionState", false)) {
|
|
Resource plc = tx.getResourceBy(TYPE_PLC, this.plcId, true);
|
|
|
|
StringParameter stateP = plc.getParameter(PARAM_CONNECTION_STATE, true);
|
|
stateP.setValue(state.name());
|
|
|
|
StringParameter stateMsgP = plc.getParameter(PARAM_CONNECTION_STATE_MSG, true);
|
|
stateMsgP.setValue(stateMsg);
|
|
|
|
tx.update(plc);
|
|
tx.commitOnClose();
|
|
}
|
|
}
|
|
|
|
private JsonObject getVersions() {
|
|
if (this.versions == null) {
|
|
this.versions = new JsonObject();
|
|
VersionQueryResult versionQueryResult = getContainer().getAgent().getVersion();
|
|
this.versions.add(AGENT_VERSION, versionQueryResult.getAgentVersion().toJson());
|
|
this.versions.add(APP_VERSION, versionQueryResult.getAppVersion().toJson());
|
|
this.versions.add(COMPONENT_VERSIONS, versionQueryResult.getComponentVersions()
|
|
.stream()
|
|
.map(ComponentVersion::toJson)
|
|
.collect(JsonArray::new, JsonArray::add, JsonArray::addAll));
|
|
}
|
|
|
|
return this.versions;
|
|
}
|
|
|
|
public JsonArray getIpAddresses() {
|
|
if (this.ipAddresses == null || this.ipAddresses.size() == 0 || (
|
|
System.currentTimeMillis() - this.ipAddressesUpdateTime > 10000L)) {
|
|
try {
|
|
this.ipAddresses = NetworkHelper.findInet4Addresses().stream().map(add -> {
|
|
String mac;
|
|
try {
|
|
mac = formatMacAddress(getByInetAddress(add).getHardwareAddress());
|
|
} catch (SocketException e) {
|
|
logger.error("Failed to get HW address for " + add.getHostAddress(), e);
|
|
mac = "(unknown)";
|
|
}
|
|
|
|
JsonObject j = new JsonObject();
|
|
j.addProperty(PARAM_HOST_NAME, add.getHostName());
|
|
j.addProperty(PARAM_IP_ADDRESS, add.getHostAddress());
|
|
j.addProperty(PARAM_MAC_ADDRESS, mac);
|
|
return j;
|
|
}).collect(JsonArray::new, JsonArray::add, JsonArray::addAll);
|
|
} catch (SocketException e) {
|
|
logger.error("Failed to enumerate IP Addresses!", e);
|
|
this.ipAddresses = new JsonArray();
|
|
}
|
|
|
|
this.ipAddressesUpdateTime = System.currentTimeMillis();
|
|
}
|
|
return this.ipAddresses;
|
|
}
|
|
|
|
@Override
|
|
public void handleNotification(PlcAddress address, Object value) {
|
|
|
|
// if not connected, then queue notifications, storing last value per address
|
|
if (!this.authenticated && address.plcAddressKey.equals(K_PLC_SERVER_CONNECTED)) {
|
|
// keep this notification after other notifications
|
|
this.notConnectedQueue.remove(address);
|
|
this.notConnectedQueue.put(address, value);
|
|
return;
|
|
}
|
|
|
|
notifyServer(address, value);
|
|
}
|
|
|
|
private PlcAddress parsePlcAddress(JsonObject telegramJ) {
|
|
if (!telegramJ.has(PARAM_RESOURCE) || !telegramJ.has(PARAM_ACTION))
|
|
throw new IllegalArgumentException("Both " + PARAM_RESOURCE + " and " + PARAM_ACTION + " is required!");
|
|
|
|
String resource = telegramJ.get(PARAM_RESOURCE).getAsString();
|
|
String action = telegramJ.get(PARAM_ACTION).getAsString();
|
|
|
|
return this.plcHandler.getPlcAddress(resource, action);
|
|
}
|
|
|
|
private static void handleFailedTelegram(JsonObject telegramJ, PlcAddress plcAddress, Exception e) {
|
|
if (plcAddress == null) {
|
|
logger.error("Failed to handle telegram: " + telegramJ, e);
|
|
telegramJ.addProperty(PARAM_STATE, PlcResponseState.Failed.name());
|
|
telegramJ.addProperty(PARAM_STATE_MSG,
|
|
"Could not evaluate PlcAddress: " + getExceptionMessage(getRootCause(e), false));
|
|
} else {
|
|
logger.error("Failed to execute telegram: " + plcAddress.toKeyAddress(), e);
|
|
telegramJ.addProperty(PARAM_STATE, PlcResponseState.Failed.name());
|
|
telegramJ.addProperty(PARAM_STATE_MSG,
|
|
"Failed to perform " + plcAddress.toKey() + ": " + getExceptionMessage(getRootCause(e), false));
|
|
}
|
|
}
|
|
|
|
@ClientEndpoint
|
|
public static class PlcGwClientEndpoint {
|
|
|
|
private final PlcGwClientHandler gwHandler;
|
|
|
|
public PlcGwClientEndpoint(PlcGwClientHandler gwHandler) {
|
|
this.gwHandler = gwHandler;
|
|
}
|
|
|
|
@OnMessage
|
|
public void onMessage(String message) {
|
|
this.gwHandler.onWsMessage(message);
|
|
}
|
|
|
|
@OnMessage
|
|
public void onPong(PongMessage message, Session session) {
|
|
this.gwHandler.onWsPong(message, session);
|
|
}
|
|
|
|
@OnOpen
|
|
public void onOpen(Session session) {
|
|
this.gwHandler.onWsOpen(session);
|
|
}
|
|
|
|
@OnClose
|
|
public void onClose(Session session, CloseReason closeReason) {
|
|
this.gwHandler.onWsClose(session, closeReason);
|
|
}
|
|
|
|
@OnError
|
|
public void onError(Session session, Throwable throwable) {
|
|
this.gwHandler.onWsError(session, throwable);
|
|
}
|
|
}
|
|
}
|