package org.yamcs.ui;
import io.netty.channel.ChannelFuture;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yamcs.YamcsException;
import org.yamcs.api.YamcsConnectionProperties;
import org.yamcs.api.rest.RestClient;
import org.yamcs.api.ws.ConnectionListener;
import org.yamcs.api.ws.WebSocketClient;
import org.yamcs.api.ws.WebSocketClientCallback;
import org.yamcs.api.ws.WebSocketRequest;
import org.yamcs.api.ws.WebSocketResponseHandler;
import org.yamcs.protobuf.Web.WebSocketServerMessage.WebSocketSubscriptionData;
import org.yamcs.protobuf.YamcsManagement.YamcsInstance;
/**
* This is like a WebsocketClient but performs reconnection and implements some callbacks to announce the state.
* to be used by all the gui data receivers (event viewer, archive browser, etc)
*
* @author nm
*
*/
public class YamcsConnector implements WebSocketClientCallback {
CopyOnWriteArrayList<ConnectionListener> connectionListeners=new CopyOnWriteArrayList<>();
CopyOnWriteArrayList<WebSocketClientCallback> subscribers = new CopyOnWriteArrayList<>();
volatile boolean connected, connecting;
protected WebSocketClient wsClient;
protected RestClient restClient;
protected YamcsConnectionProperties connectionParams;
static Logger log= LoggerFactory.getLogger(YamcsConnector.class);
private boolean retry = true;
private boolean reconnecting = false;
final private ExecutorService executor = Executors.newSingleThreadScheduledExecutor();
List<YamcsInstance> instances;
final String aplicationName;
/**
*
* @param aplicationName - application name that is passed on via web socket (as UserAgent) to be displayed in the YamcsMonitor
*/
public YamcsConnector(String aplicationName) {
this(true, aplicationName);
}
public YamcsConnector(boolean retry, String aplicationName) {
this.retry = retry;
this.aplicationName = aplicationName;
}
public void addConnectionListener(ConnectionListener connectionListener) {
this.connectionListeners.add(connectionListener);
}
public List<String> getYamcsInstances() {
if(instances==null) return null;
return instances.stream().map(r-> r.getName()).collect(Collectors.toList());
}
public Future<YamcsConnectionProperties> connect(YamcsConnectionProperties cp) {
System.out.println("connecting to yamcs");
this.connectionParams = cp;
return doConnect();
}
private FutureTask<YamcsConnectionProperties> doConnect() {
if(connected) disconnect();
restClient = new RestClient(connectionParams);
restClient.setAutoclose(false);
wsClient = new WebSocketClient(connectionParams, this);
wsClient.setUserAgent(aplicationName);
FutureTask<YamcsConnectionProperties> future=new FutureTask<>(new Runnable() {
@Override
public void run() {
String connectingTo = connectionParams.getHost()+":"+connectionParams.getPort();
//connect to yamcs
int maxAttempts=10;
try {
if(reconnecting && !retry) {
log.warn("Retries are disabled, cancelling reconnection");
reconnecting = false;
return;
}
connecting=true;
for(ConnectionListener cl:connectionListeners) {
cl.connecting(connectingTo);
}
for(int i=0;i<maxAttempts;i++) {
try {
log.debug("Connecting to {} attempt {}", connectingTo, i);
instances = restClient.blockingGetYamcsInstances();
if(instances==null || instances.isEmpty()) {
log.warn("No configured yamcs instance");
return;
}
String defaultInstanceName = instances.get(0).getName();
String instanceName = defaultInstanceName;
if(connectionParams.getInstance()!=null){ //check if the instance saved in properties exists, otherwise use the default one
instanceName = instances.stream().map(yi->yi.getName()).filter(s -> s.equals(connectionParams.getInstance())).findFirst().orElse(defaultInstanceName);
}
connectionParams.setInstance(instanceName);
ChannelFuture future = wsClient.connect();
future.get(5000, TimeUnit.MILLISECONDS);
//now the TCP connection is established but we have to wait for the websocket to be setup
// the connected callback will handle that
return;
} catch (Exception e) {
// For anything other than a security exception, re-try
for(ConnectionListener cl:connectionListeners) {
cl.log("Connection to "+connectionParams.getHost()+":"+connectionParams.getPort()+" failed :"+e.getMessage());
}
log.warn("Connection to "+connectionParams.getHost()+":"+connectionParams.getPort()+" failed :", e);
Thread.sleep(5000);
}
}
connecting=false;
for(ConnectionListener cl:connectionListeners) {
cl.log(maxAttempts+" connection attempts failed, giving up.");
cl.connectionFailed(connectingTo, new YamcsException( maxAttempts+" connection attempts failed, giving up." ));
}
log.warn(maxAttempts+" connection attempts failed, giving up.");
} catch(InterruptedException e){
for(ConnectionListener cl:connectionListeners)
cl.connectionFailed(connectingTo, new YamcsException( "Thread interrupted", e ));
}
};
}, connectionParams);
executor.submit(future);
return future;
}
@Override
public void disconnected() {
String msg ="Connection to "+connectionParams.getHost()+":"+connectionParams.getPort()+" lost";
if(connected) log.warn(msg);
for(ConnectionListener cl:connectionListeners) {
if(connected) cl.log(msg);
cl.disconnected();
}
}
public void disconnect() {
log.info("Disconnection requested");
if(!connected)
return;
wsClient.disconnect();
connected=false;
}
public String getUrl() {
return connectionParams.webSocketURI().toString();
}
public boolean isConnected() {
return connected;
}
public boolean isConnecting() {
return connecting;
}
public YamcsConnectionProperties getConnectionParams() {
return connectionParams;
}
public ExecutorService getExecutor() {
return executor;
}
@Override
public void onMessage(WebSocketSubscriptionData data) {
for(WebSocketClientCallback client: subscribers) {
client.onMessage(data);
}
}
//called when the websocket has been setup
@Override
public void connected() {
connected=true;
String connectingTo = connectionParams.getHost()+":"+connectionParams.getPort();
for(ConnectionListener cl:connectionListeners) {
cl.connected(connectingTo);
}
}
public WebSocketClient getWebSocketClient() {
return wsClient;
}
public RestClient getRestClient() {
return restClient;
}
/**
* Sends the subscription via websocket and register the client to the subscriber lists
*
* Note that currently Yamcs does not send back the original requestId with the subscription data so it's not possible to send to clients only the data they subscribed to.
* So the clients have to sort out the data they need and drop the data they don't need.
*
* @param wsr
* @param client
* @param wsrh - any error related to the request will be sent here
*/
public void performSubscription(WebSocketRequest wsr, WebSocketClientCallback client, WebSocketResponseHandler wsrh) {
if(!subscribers.contains(client)){
subscribers.add(client);
}
wsClient.sendRequest(wsr, wsrh);
}
public CompletableFuture<Void> performSubscription(WebSocketRequest wsr, WebSocketClientCallback client) {
if(!subscribers.contains(client)){
subscribers.add(client);
}
return wsClient.sendRequest(wsr);
}
}