/*******************************************************************************
* Copyright (c) 2013 Rene Schneider, GEBIT Solutions GmbH and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*******************************************************************************/
package de.gebit.integrity.remoting.transport;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import de.gebit.integrity.remoting.transport.messages.AbstractMessage;
/**
* A server endpoint. This endpoint listens on a specified port and host IP for incoming connections from client
* endpoints. When a connection is established, it spawns a new {@link Endpoint} for that specific connection and
* continues listening for more connections.
*
* @author Rene Schneider - initial API and implementation
*
*/
public class ServerEndpoint {
/**
* The server socket.
*/
private ServerSocket serverSocket;
/**
* The message processor map.
*/
private Map<Class<? extends AbstractMessage>, MessageProcessor<?>> messageProcessors;
/**
* The thread waiting for incoming connections.
*/
private ConnectionWaiter connectionWaiter;
/**
* A list of active endpoints.
*/
private List<Endpoint> endpoints = new LinkedList<Endpoint>();
/**
* Whether this server endpoint is in the process of termination.
*/
private boolean closing;
/**
* The shutdown hook thread that was registered (if any).
*/
private ShutdownHookThread shutdownHookThread;
/**
* Creates a new server endpoint that listens on a specified port/IP.
*
* @param aHostIP
* the host IP to listen on
* @param aPort
* the port to bind to
* @param aProcessorMap
* the map of processors to use for processing incoming messages
* @param aClassLoader
* the classloader to use when deserializing objects
* @param anIsForkFlag
* whether this server endpoint is serving inside an Integrity fork process
* @throws UnknownHostException
* @throws IOException
*/
public ServerEndpoint(String aHostIP, int aPort,
Map<Class<? extends AbstractMessage>, MessageProcessor<?>> aProcessorMap, ClassLoader aClassLoader,
boolean anIsForkFlag)
throws UnknownHostException, IOException {
messageProcessors = aProcessorMap;
serverSocket = new ServerSocket(aPort, 0, Inet4Address.getByName(aHostIP));
connectionWaiter = new ConnectionWaiter(aClassLoader);
connectionWaiter.start();
if (anIsForkFlag) {
// Try to detect problematic force-shutdown situations within forks
shutdownHookThread = new ShutdownHookThread();
Runtime.getRuntime().addShutdownHook(shutdownHookThread);
}
}
public boolean isActive() {
return serverSocket.isBound() && !serverSocket.isClosed();
}
/**
* Closes the server endpoint and all endpoints currently active.
*
* @param anEmptyOutputQueueFlag
* whether output queues to the endpoints shall be emptied before closing the connection.
*/
public void closeAll(boolean anEmptyOutputQueueFlag) {
if (isActive()) {
closing = true;
try {
serverSocket.close();
} catch (IOException exc) {
// don't care
}
synchronized (endpoints) {
for (Endpoint tempEndpoint : endpoints) {
if (tempEndpoint.isActive()) {
tempEndpoint.close(anEmptyOutputQueueFlag);
}
}
endpoints.clear();
}
}
if (shutdownHookThread != null) {
try {
Runtime.getRuntime().removeShutdownHook(shutdownHookThread);
} catch (IllegalStateException exc) {
// ignored - may be thrown if this code is run during VM shutdown, but we don't care about that
}
}
}
private class ConnectionWaiter extends Thread {
/**
* The classloader to use when deserializing objects.
*/
private ClassLoader classLoader;
/**
* Creates a new instance.
*
* @param aClassLoader
* the classloader to use when deserializing objects
*/
ConnectionWaiter(ClassLoader aClassLoader) {
super("Integrity - Server Endpoint Connection Waiter");
classLoader = aClassLoader;
}
@Override
public void run() {
while (isActive()) {
try {
Socket tempSocket = serverSocket.accept();
tempSocket.setSoLinger(true, 60);
synchronized (endpoints) {
endpoints.add(new Endpoint(tempSocket, messageProcessors, new EndpointListener() {
@Override
public void onConnectionLost(Endpoint anEndpoint) {
synchronized (endpoints) {
endpoints.remove(anEndpoint);
}
}
@Override
public void onClosed(Endpoint anEndpoint) {
// we'll remove it in the outer class
}
}, classLoader));
}
} catch (IOException exc) {
if (!closing) {
exc.printStackTrace();
}
}
}
}
}
/**
* Broadcasts a message to all active endpoints.
*
* @param aMessage
* the message
*/
public void broadcastMessage(AbstractMessage aMessage) {
synchronized (endpoints) {
for (Endpoint tempEndpoint : endpoints) {
if (tempEndpoint.isActive()) {
tempEndpoint.sendMessage(aMessage);
}
}
}
}
private class ShutdownHookThread extends Thread {
ShutdownHookThread() {
super("Integrity - Server Endpoint Shutdown Hook");
}
@Override
public void run() {
synchronized (endpoints) {
for (Endpoint tempEndpoint : endpoints) {
if (!tempEndpoint.isDisconnectRequested()) {
System.err
.println("ENCOUNTERED A NON-FINALIZED INTEGRITY CONTROL CONNECTION DURING VM SHUTDOWN! "
+ "THIS IS NOT GOOD - IT INDICATES THAT THIS FORK HAS BEEN FORCEFULLY KILLED. "
+ "KILLING A FORK BEFORE IT HAD A CHANCE TO SHUT DOWN ALL INCOMING CONTROL "
+ "CONNECTIONS MAY RESULT IN VARIOUS SEVERE INTEGRITY TEST EXECUTION FAILURES!");
boolean tempFoundCulprit = false;
for (Entry<Thread, StackTraceElement[]> tempEntry : Thread.getAllStackTraces().entrySet()) {
for (StackTraceElement tempElement : tempEntry.getValue()) {
if (!tempFoundCulprit && "java.lang.Shutdown".equals(tempElement.getClassName())
&& "exit".equals(tempElement.getMethodName())) {
System.err.println(
"FOUND AN EXECUTION PATH THAT IS MOST LIKELY RESPONSIBLE FOR THIS VM "
+ "SHUTDOWN - HERE COMES THE STACK TRACE OF THREAD '"
+ tempEntry.getKey() + "':");
tempFoundCulprit = true;
}
if (tempFoundCulprit) {
System.err.println("\t" + tempElement.toString());
}
}
if (tempFoundCulprit) {
break;
}
}
return;
}
}
}
}
}
}