/** * Copyright 2011-2013 Terracotta, Inc. * Copyright 2011-2013 Oracle, Inc. * Copyright 2016 headissue GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jsr107.tck.support; import java.io.EOFException; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.net.UnknownHostException; import java.util.Enumeration; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; /** * A rudimentary multi-threaded {@link Socket}-based {@link Server} that can * handle, using {@link OperationHandler}s, {@link Operation}s invoked by * {@link Client}s. * * @author Brian Oliver * @author Jens Wilke * @see Client * @see Operation * @see OperationHandler */ public class Server implements AutoCloseable { /** * If false, always communicate over TCP and don't try to use direct method in the local VM */ private static final boolean ALLOW_DIRECT_CALLS = System.getProperty("org.jsr107.tck.support.server.alwaysUseTcp") == null; /** * If a server and socket is unused, put it in a queue and reuse it. * This way we can save the TCP connection overhead and thread start if * it is possible to communicate directly within the local machine. */ private static final Queue<SocketThread> SOCKET_THREADS = new LinkedBlockingDeque<>(10); /** * Contains the servers that run on the respective port. The client can lookup its server * in the hash table. If it is present, it runs in the local VM. If it is not present * the client must connect via TCP. */ private static final ConcurrentHashMap<Integer, Server> PORT_2_SERVER = new ConcurrentHashMap<>(); /** * Returns the server for this port, if running in the local machine. */ public static Server lookupServerAtLocalMachine(int port) { return PORT_2_SERVER.get(port); } /** * Logger */ public static final Logger LOG = Logger.getLogger(Server.class.getName()); /** * Special operation to signal the server that the client has been closed.  */ public static final Operation<Void> CLOSE_OPERATION = new Operation<Void>() { public String getType() { return "CLOSE"; } /**  * The connections are closed by the server upon receiving the closed operation.  * We need to block in the client until the server performed the close, to make  * sure the server has removed the client from the client collection map.  *  * <p>This is executed in the client after the close command is sent.  *  * @see Client#invoke(Operation)  */ public Void onInvoke(final ObjectInputStream ois, final ObjectOutputStream oos) throws IOException { try { ois.readByte(); throw new IOException("Unexpected data received from the server after close."); } catch (EOFException expected) { // connection successfully closed by the server } return null; } }; /** * The {@link OperationHandler}s by operation. */ private ConcurrentHashMap<String, OperationHandler> operationHandlers; /** * The {@link Thread} that will manage accepting {@link Client} connections. * <p> * When this is <code>null</code> the {@link Server} is not running. */ private SocketThread serverThread; /** * Should the running {@link Server} terminate as soon as possible? */ private AtomicBoolean isTerminating = new AtomicBoolean(false); private ServerSocket serverSocket; /** * A map of {@link ClientConnection} by connection number. */ private ConcurrentHashMap<Integer, ClientConnection> clientConnections; private int serverDirectUsage = 0; /** * Construct a {@link Server} that will accept {@link Client} connections * and requests on the specified port. * * @param port the port on which to accept {@link Client} connections and requests */ public Server(int port) { this.operationHandlers = new ConcurrentHashMap<String, OperationHandler>(); this.clientConnections = new ConcurrentHashMap<Integer, ClientConnection>(); } /** * Registers the specified {@link OperationHandler} for an operation. * * @param handler the {@link OperationHandler} */ public void addOperationHandler(OperationHandler handler) { this.operationHandlers.put(handler.getType(), handler); } /** * Opens and starts the {@link Server}. * <p> * Does nothing if the {@link Server} is already open. * * @return the {@link InetAddress} on which the {@link Server} * is accepting requests from {@link Client}s. * @throws IOException if not able to create ServerSocket */ public synchronized InetAddress open() throws IOException { if (serverThread == null) { if (ALLOW_DIRECT_CALLS) { serverThread = SOCKET_THREADS.poll(); } if (serverThread == null) { serverSocket = createServerSocket(); serverThread = new SocketThread(); serverThread.serverSocket = serverSocket; serverThread.setBoundServer(this); serverThread.start(); } else { serverSocket = serverThread.serverSocket; serverThread.setBoundServer(this); assert !serverThread.wasUsed(); } if (ALLOW_DIRECT_CALLS) { PORT_2_SERVER.put(serverSocket.getLocalPort(), this); } } return getInetAddress(); } /** * Obtains the {@link InetAddress} on which the {@link Server} is listening. * * @return the {@link InetAddress} */ public synchronized InetAddress getInetAddress() { if (serverThread != null) { try { return getServerInetAddress(); } catch (SocketException e) { return serverSocket.getInetAddress(); } catch (UnknownHostException e) { return serverSocket.getInetAddress(); } } else { throw new IllegalStateException("Server is not open"); } } /** * Obtains the port on which the {@link Server} is listening. * * @return the port */ public synchronized int getPort() { if (serverThread != null) { return serverSocket.getLocalPort(); } else { throw new IllegalStateException("Server is not open"); } } public synchronized void clientConnectedDirectly() { serverDirectUsage++; } public synchronized void closeWasCalledOnClient() { if (serverDirectUsage == 0) { throw new IllegalStateException( "customization already closed, class: " + this.getClass().getSimpleName()); } serverDirectUsage--; } /** * Stops the {@link Server}. * <p> * Does nothing if the {@link Server} is already stopped. */ public synchronized void close() { if (serverThread != null) { PORT_2_SERVER.remove(getPort()); if (ALLOW_DIRECT_CALLS) { if (serverDirectUsage > 0) { throw new IllegalStateException( "The close() method was not called. " + "Customizations implementing Closeable need to be closed. " + "See https://github.com/jsr107/jsr107tck/issues/100" + ", server type " + this.getClass().getSimpleName() ); } } if (!serverThread.wasUsed()) { SOCKET_THREADS.offer(serverThread); } else { //we're now terminating isTerminating.set(true); if (clientConnections.size() > 0) { LOG.warning("Open client connections: " + clientConnections); throw new IllegalStateException( "Excepting no open client connections. " + "Customizations implementing Closeable need to be closed. " + "See https://github.com/jsr107/jsr107tck/issues/100" + ", server type " + this.getClass().getSimpleName() ); } //stop the server socket try { serverSocket.close(); } catch (IOException e) { //failed to close the server socket - but we don't care } serverSocket = null; //interrupt the server thread serverThread.interrupt(); } serverThread = null; //stop the clients for (ClientConnection clientConnection : clientConnections.values()) { clientConnection.close(); } this.clientConnections = new ConcurrentHashMap<Integer, ClientConnection>(); } } /** * Asynchronously handles {@link Client} requests via a {@link Socket} using the * defined {@link OperationHandler}s. */ private static class ClientConnection extends Thread implements AutoCloseable { private Server server; /** * The {@link ClientConnection} identity. */ private int identity; /** * The {@link Socket} to the {@link Client}. */ private Socket socket; /** * Constructs a {@link ClientConnection}. * * @param identity the identity for the {@link ClientConnection} * @param socket the {@link Socket} on which to receive and respond to * {@link Client} requests */ public ClientConnection(Server server, int identity, Socket socket) { this.server = server; this.identity = identity; this.socket = socket; } /** * Obtains the identity for the {@link ClientConnection}. * * @return the identity */ public int getIdentity() { return this.identity; } /** * {@inheritDoc} */ @Override public void run() { try { ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream()); ObjectInputStream ois = new ObjectInputStream(socket.getInputStream()); while (true) { try { String operation = (String) ois.readObject(); if (CLOSE_OPERATION.getType().equals(operation)) { // regular close, remove before closing server.clientConnections.remove(identity); // connection close means we acknowledge to the client and the client may // complete the close operation. socket.close(); socket = null; break; } server.executeOperation(operation, ois, oos); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } catch (IOException e) { //any error closes the connection } finally { if (socket != null) { try { socket.close(); } catch (IOException e) { //failed to close the socket - but we don't care } } //remove this from the server server.clientConnections.remove(identity); } } /** * {@inheritDoc} */ public void close() { try { socket.close(); } catch (IOException e) { //failed to close the socket - but we don't care } finally { socket = null; } } } public void executeOperation(String operation, ObjectInputStream ois, ObjectOutputStream oos) throws ClassNotFoundException, IOException { OperationHandler handler = operationHandlers.get(operation); if (handler != null) { handler.onProcess(ois, oos); } else { throw new IllegalArgumentException("no handler for: " + operation); } } private static InetAddress serverSocketAddress = null; private ServerSocket createServerSocket() throws IOException { final int ephemeralPort = 0; // JW: change from original TCK: always use ephemeral port! ServerSocket result = new ServerSocket(ephemeralPort, 50, getServerInetAddress()); LOG.log(Level.INFO, "Starting " + this.getClass().getCanonicalName() + " server at address:" + getServerInetAddress() + " port:" + result.getLocalPort()); return result; } /** * To support distributed testing, return a non-loopback address if available. * <p> * By default, on a machine with multiple network interfaces, the appropriate * ip address is selected based on values of java network system properties java.net.preferIPv4Stack * and java.net.preferIPv6Addresses. * <p> * A user can override the automated selection by specifying a network interface with * system property org.jsr107.tck.support.server.networkinterface set to the * display name of the network interface or explicitly setting the server address with * system property org.jsr107.tck.support.server.address. The address value may be either * ipv4 or ipv6, the value should be consistent with the values of the java network properties q * mentioned above. * <p> * @return remote addressable inet address * @throws SocketException * @throws UnknownHostException */ private static InetAddress getServerInetAddress() throws SocketException, UnknownHostException { if (serverSocketAddress == null) { boolean preferIPV4Stack = Boolean.getBoolean("java.net.preferIPv4Stack"); boolean preferIPV6Addresses = Boolean.getBoolean("java.net.preferIPv6Addresses") && !preferIPV4Stack; String serverAddress = System.getProperty("org.jsr107.tck.support.server.address"); if (serverAddress != null) { try { serverSocketAddress = InetAddress.getByName(serverAddress); } catch (UnknownHostException e) { // ignore LOG.log(Level.WARNING, "ignoring system property org.jsr107.tck.support.server.address due to exception", e); } } NetworkInterface serverSocketNetworkInterface = null; if (serverSocketAddress == null) { String niName = System.getProperty("org.jsr107.tck.support.server.networkinterface"); try { serverSocketNetworkInterface = niName == null ? null : NetworkInterface.getByName(niName); if (serverSocketNetworkInterface != null) { serverSocketAddress = getFirstNonLoopbackAddress(serverSocketNetworkInterface, preferIPV4Stack, preferIPV6Addresses); } if (serverSocketAddress == null) { LOG.log(Level.WARNING, "ignoring system property org.jsr107.tck.support.server.networkinterface with value:" + niName); } } catch (SocketException e) { LOG.log(Level.WARNING, "ignoring system property org.jsr107.tck.support.server.networkinterface due to exception", e); } } if (serverSocketAddress == null) { serverSocketAddress = getFirstNonLoopbackAddress(preferIPV4Stack, preferIPV6Addresses); } if (serverSocketAddress == null) { LOG.warning("no remote ip address available so only possible to test using loopback address."); serverSocketAddress = InetAddress.getLocalHost(); } } return serverSocketAddress; } /** * Get non-loopback address. InetAddress.getLocalHost() does not work on machines without static ip address. * * @param preferIPv4 true iff require IPv4 addresses only * @param preferIPv6 true iff prefer IPv6 addresses * @return nonLoopback {@link InetAddress} * @throws SocketException */ private static InetAddress getFirstNonLoopbackAddress(boolean preferIPv4, boolean preferIPv6) throws SocketException { InetAddress result = null; Enumeration en = NetworkInterface.getNetworkInterfaces(); while (result == null && en.hasMoreElements()) { result = getFirstNonLoopbackAddress((NetworkInterface) en.nextElement(), preferIPv4, preferIPv6); } return result; } /** * Get non-loopback address. InetAddress.getLocalHost() does not work on machines without static ip address. * * @param ni target network interface * @param preferIPv4 true iff require IPv4 addresses only * @param preferIPv6 true iff prefer IPv6 addresses * @return nonLoopback {@link InetAddress} * @throws SocketException */ private static InetAddress getFirstNonLoopbackAddress(NetworkInterface ni, boolean preferIPv4, boolean preferIPv6) throws SocketException { InetAddress result = null; // skip virtual interface name, PTP and non-running interface. if (ni.isVirtual() || ni.isPointToPoint() || ! ni.isUp()) { return result; } LOG.info("Interface name is: " + ni.getDisplayName()); for (Enumeration en2 = ni.getInetAddresses(); en2.hasMoreElements(); ) { InetAddress addr = (InetAddress) en2.nextElement(); if (!addr.isLoopbackAddress()) { if (addr instanceof Inet4Address) { if (preferIPv6) { continue; } result = addr; break; } if (addr instanceof Inet6Address) { if (preferIPv4) { continue; } result = addr; break; } } } return result; } static AtomicInteger connectionId = new AtomicInteger(); static class SocketThread extends Thread { private AtomicBoolean wasUsed = new AtomicBoolean(); ServerSocket serverSocket; volatile Server boundServer; public void setBoundServer(final Server _boundServer) { boundServer = _boundServer; } public boolean wasUsed() { return wasUsed.get(); } @Override public void run() { try { while (!boundServer.isTerminating.get()) { Socket socket = serverSocket.accept(); wasUsed.set(true); ClientConnection clientConnection = new ClientConnection(boundServer, connectionId.incrementAndGet(), socket); boundServer.clientConnections.put(clientConnection.getIdentity(), clientConnection); clientConnection.start(); } } catch (IOException e) { // ignore } finally { wasUsed.set(true); } } } }