/* * Sun Public License * * The contents of this file are subject to the Sun Public License Version * 1.0 (the "License"). You may not use this file except in compliance with * the License. A copy of the License is available at http://www.sun.com/ * * The Original Code is the SLAMD Distributed Load Generation Engine. * The Initial Developer of the Original Code is Neil A. Wilson. * Portions created by Neil A. Wilson are Copyright (C) 2004-2010. * Some preexisting portions Copyright (C) 2002-2006 Sun Microsystems, Inc. * All Rights Reserved. * * Contributor(s): Neil A. Wilson */ package com.slamd.clientmanager; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; import java.security.Security; import java.util.ArrayList; import java.util.Iterator; import javax.net.ssl.SSLSocketFactory; import com.slamd.asn1.ASN1Element; import com.slamd.asn1.ASN1Exception; import com.slamd.asn1.ASN1Reader; import com.slamd.asn1.ASN1Writer; import com.slamd.client.Client; import com.slamd.client.ClientMessageWriter; import com.slamd.common.Constants; import com.slamd.jobs.JSSEBlindTrustSocketFactory; import com.slamd.message.ClientManagerHelloMessage; import com.slamd.message.HelloResponseMessage; import com.slamd.message.Message; import com.slamd.message.ServerShutdownMessage; import com.slamd.message.StartClientRequestMessage; import com.slamd.message.StartClientResponseMessage; import com.slamd.message.StopClientRequestMessage; import com.slamd.message.StopClientResponseMessage; /** * This class defines a client manager that will manage the process of starting * clients independent of the SLAMD server itself. * * * @author Neil A. Wilson */ public class ClientManager { /** * The maximum length of time in milliseconds that any part of this client * manager should block at any one time. */ public static final int MAX_BLOCK_TIME = 5000; /** * The length of time between checks to determine whether the SLAMD server is * available if it was detected that it has been taken offline. */ public static final int SERVER_DOWN_CHECK_TIME = 30000; // The set of processes associated with this client manager. ArrayList<Process> processList; // The ASN.1 reader used to read messages from the server. ASN1Reader asn1Reader; // The ASN.1 writer used to write messages to the server. ASN1Writer asn1Writer; // Indicates whether the client manager should blindly trust any SSL // certificate presented by the SLAMD server. boolean blindTrust; // Indicates whether the client manager is currently connected to the SLAMD // server. boolean connected; // Indicates whether the client manager should be stopped. boolean stopClientManager; // Indicates whether the client manager should use SSL to communicate with // the SLAMD server. boolean useSSL; // The buffer used to read data from the client processes. byte[] readBuffer; // The client message writer that will be used for writing messages about the // progress of the client manager. ClientMessageWriter messageWriter; // The number of client instances to automatically create upon connecting to // the SLAMD server. int autoCreateClients; // The maximum number of clients that may be created on this system. int maxClients; // The ID of the next message that should be sent to the SLAMD server. int nextMessageID; // The port number for the SLAMD server. int serverPort; // The socket used to communicate with the SLAMD server. Socket socket; // The local address the client should use when connecting to the server. String clientAddress; // The client ID for this client manager. String clientID; // The address of the SLAMD server. String serverAddress; // The location of the JSSE key store. String sslKeyStore; // The password required to access the JSSE key store. String sslKeyStorePassword; // The location of the JSSE trust store. String sslTrustStore; // The password required to access the JSSE trust store. String sslTrustStorePassword; // The command to use to start an instance of the client. String startCommand; /** * Creates a new client manager that is intended to work with the specified * SLAMD server. * * @param clientID The client ID that should be used for the * client. It may be {@code null} if the * client ID should be automatically selected. * @param clientAddress The IP address on the client system from * which the connection should originate. It * may be {@code null} if the client address * should be automatically selected. * @param serverAddress The address of the SLAMD server. * @param serverPort The port number of the SLAMD server. * @param useSSL Indicates whether to use SSL to communicate * with the SLAMD server. * @param blindTrust Indicates whether to blindly trust any SSL * certificate presented by the SLAMD server. * @param sslKeyStore The location of the JSSE key store. * @param sslKeyStorePassword The password required to access the SSL key * store. * @param sslTrustStore The location of the JSSE trust store. * @param sslTrustStorePassword The password required to access the SSL * trust store. * @param maxClients The maximum number of clients that may be * created on this system. * @param startCommand The command to execute to start a single * client instance. * @param messageWriter The message writer to use for output. */ public ClientManager(String clientID, String clientAddress, String serverAddress, int serverPort, boolean useSSL, boolean blindTrust, String sslKeyStore, String sslKeyStorePassword, String sslTrustStore, String sslTrustStorePassword, int maxClients, String startCommand, ClientMessageWriter messageWriter) { this.clientAddress = clientAddress; this.serverAddress = serverAddress; this.serverPort = serverPort; this.useSSL = useSSL; this.blindTrust = blindTrust; this.sslKeyStore = sslKeyStore; this.sslKeyStorePassword = sslKeyStorePassword; this.sslTrustStore = sslTrustStore; this.sslTrustStorePassword = sslTrustStorePassword; this.maxClients = maxClients; this.startCommand = startCommand; this.messageWriter = messageWriter; autoCreateClients = 0; readBuffer = new byte[1024]; connected = false; stopClientManager = false; nextMessageID = 1; processList = new ArrayList<Process>(); socket = null; asn1Reader = null; asn1Writer = null; if ((clientID == null) || (clientID.length() == 0)) { try { clientID = InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException uhe) { try { clientID = InetAddress.getLocalHost().getHostAddress(); } catch (Exception e) { clientID = "unknown"; } } } this.clientID = clientID; if (useSSL && (! blindTrust)) { Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider()); System.setProperty("java.protocol.handler.pkgs", "com.sun.net.ssl.internal.www.protocol"); } if ((sslKeyStore != null) && (sslKeyStore.length() > 0)) { System.setProperty(Constants.SSL_KEY_STORE_PROPERTY, sslKeyStore); } if ((sslKeyStorePassword != null) && (sslKeyStorePassword.length() > 0)) { System.setProperty(Constants.SSL_KEY_PASSWORD_PROPERTY, sslKeyStorePassword); } if ((sslTrustStore != null) && (sslTrustStore.length() > 0)) { System.setProperty(Constants.SSL_TRUST_STORE_PROPERTY, sslTrustStore); } if ((sslTrustStorePassword != null) && (sslTrustStorePassword.length() > 0)) { System.setProperty(Constants.SSL_TRUST_PASSWORD_PROPERTY, sslTrustStorePassword); } } /** * Specifies the number of client instances to create automatically when * establishing a connection to the SLAMD server. * * @param autoCreateClients The number of client instances to create * automatically when establishing a connection to * the SLAMD server. */ public void setAutoCreateClients(int autoCreateClients) { this.autoCreateClients = autoCreateClients; } /** * Establishes a connection to the SLAMD server and then operates in a loop * waiting for requests to arrive. */ public void run() { // Create a variable that we will use for detecting consecutive failures // when attempting to read requests from the SLAMD server. boolean consecutiveFailures = false; // Enter a loop that waits for messages to arrive from the SLAMD server. // This needs to handle the SLAMD server shutting down so that it can // reconnect to it a short time after it comes back up. while (! stopClientManager) { // See if we need to re-establish the connection to the SLAMD server. If // so, then do it. If the reconnect attempt fails, then sleep and try // again. if (! connected) { // Try to establish the socket to the SLAMD server. If this fails, // then the server is probably offline. Just sleep for a little bit and // try again. messageWriter.writeVerbose("Attempting to establish connection to " + serverAddress + ':' + serverPort); if (useSSL) { if (blindTrust) { try { JSSEBlindTrustSocketFactory socketFactory = new JSSEBlindTrustSocketFactory(); if ((clientAddress == null) || (clientAddress.length() == 0)) { socket = socketFactory.makeSocket(serverAddress, serverPort); } else { InetAddress localAddress = InetAddress.getByName(clientAddress); socket = socketFactory.createSocket(serverAddress, serverPort, localAddress, 0); } asn1Reader = new ASN1Reader(socket); asn1Writer = new ASN1Writer(socket); socket.setSoTimeout(MAX_BLOCK_TIME); } catch (Exception e) { messageWriter.writeVerbose("SSL blind trust connect attempt " + "failed: " + e); sleepBeforeReconnectAttempt(); continue; } } else { try { SSLSocketFactory socketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); if ((clientAddress == null) || (clientAddress.length() == 0)) { socket = socketFactory.createSocket(serverAddress, serverPort); } else { InetAddress localAddress = InetAddress.getByName(clientAddress); socket = socketFactory.createSocket(serverAddress, serverPort, localAddress, 0); } asn1Reader = new ASN1Reader(socket); asn1Writer = new ASN1Writer(socket); socket.setSoTimeout(MAX_BLOCK_TIME); } catch (Exception e) { messageWriter.writeVerbose("SSL connect attempt failed: " + e); sleepBeforeReconnectAttempt(); continue; } } } else { try { if ((clientAddress == null) || (clientAddress.length() == 0)) { socket = new Socket(serverAddress, serverPort); } else { InetAddress localAddress = InetAddress.getByName(clientAddress); socket = new Socket(serverAddress, serverPort, localAddress, 0); } asn1Reader = new ASN1Reader(socket); asn1Writer = new ASN1Writer(socket); socket.setSoTimeout(MAX_BLOCK_TIME); } catch (Exception e) { messageWriter.writeVerbose("Connect attempt failed: " + e); sleepBeforeReconnectAttempt(); continue; } } // Send the hello request message to the SLAMD server. messageWriter.writeVerbose("Attempting to send a hello request"); ClientManagerHelloMessage helloMessage = new ClientManagerHelloMessage(nextMessageID(), Client.SLAMD_CLIENT_VERSION, clientID, maxClients); try { asn1Writer.writeElement(helloMessage.encode()); } catch (Exception e) { messageWriter.writeVerbose("Hello request attempt failed: " + e); sleepBeforeReconnectAttempt(); continue; } // Read the hello response message. messageWriter.writeVerbose("Attempting to read the hello response"); HelloResponseMessage helloResponse = null; try { ASN1Element element = asn1Reader.readElement(Constants.MAX_BLOCKING_READ_TIME); helloResponse = (HelloResponseMessage) Message.decode(element); } catch (Exception e) { messageWriter.writeVerbose("Unable to read hello response: " + e); sleepBeforeReconnectAttempt(); continue; } // Interpret the hello response. if (helloResponse.getResponseCode() == Constants.MESSAGE_RESPONSE_SUCCESS) { messageWriter.writeMessage("Successfully connected to the SLAMD " + "server at " + serverAddress + ':' + serverPort); connected = true; consecutiveFailures = false; startClients(autoCreateClients); } else { boolean stillAvailable = true; switch (helloResponse.getResponseCode()) { case Constants.MESSAGE_RESPONSE_UNKNOWN_AUTH_ID: case Constants.MESSAGE_RESPONSE_INVALID_CREDENTIALS: case Constants.MESSAGE_RESPONSE_UNSUPPORTED_AUTH_TYPE: case Constants.MESSAGE_RESPONSE_UNSUPPORTED_CLIENT_VERSION: case Constants.MESSAGE_RESPONSE_UNSUPPORTED_SERVER_VERSION: case Constants.MESSAGE_RESPONSE_CLIENT_REJECTED: stillAvailable = false; break; } messageWriter.writeMessage("Hello request rejected by SLAMD server " + "-- result code " + helloResponse.getResponseCode() + ", response message \"" + helloResponse.getResponseMessage() + '"'); if (stillAvailable) { sleepBeforeReconnectAttempt(); } else { messageWriter.writeMessage("This client manager will shut down."); stopClientManager = true; } continue; } } // See if the SLAMD server has issued a request to the client manager. ASN1Element element = null; try { element = asn1Reader.readElement(); if (element == null) { messageWriter.writeMessage("Client manager connection closed by " + "the SLAMD server"); connected = false; for (final Process p : processList) { try { p.destroy(); } catch (final Exception e) {} } processList.clear(); continue; } Message message = Message.decode(element); if (message instanceof StartClientRequestMessage) { messageWriter.writeMessage("Received a StartClient request"); messageWriter.writeVerbose(message.toString()); startClients((StartClientRequestMessage) message); } else if (message instanceof StopClientRequestMessage) { messageWriter.writeMessage("Received a StopClient request"); messageWriter.writeVerbose(message.toString()); stopClients((StopClientRequestMessage) message); } else if (message instanceof ServerShutdownMessage) { messageWriter.writeMessage("Received a ServerShutdown notification"); messageWriter.writeVerbose(message.toString()); disconnect(); sleepBeforeReconnectAttempt(); } else { messageWriter.writeMessage("Unsupported message type received: " + message.getClass().getName()); } consecutiveFailures = false; } catch (InterruptedIOException iioe) { // This just means that the read attempt timed out. No big deal, and no // need to log anything. } catch (Exception e) { // This is more significant. It could mean that there is a problem // decoding the message received (for which there will be no tolerance // and the connection will be terminated immediately, since it's likely // that we wouldn't know where to start looking for the next element), // that an I/O problem occurred (in which case the connection will be // closed if this is the second such failure in so many attempts), or // some other unforeseen problem occurred (which will just be ignored). if (e instanceof ASN1Exception) { messageWriter.writeMessage("Disconnecting from the SLAMD server " + "due to an ASN.1 decoding problem: " + e); disconnect(); } else if (e instanceof IOException) { if (consecutiveFailures) { messageWriter.writeMessage("Disconnecting from the SLAMD server " + "due to consecutive I/O failures: " + e); disconnect(); } else { consecutiveFailures = true; } } else { messageWriter.writeVerbose("Ignoring uncaught exception: " + e); e.printStackTrace(); } } // Iterate through all the clients. Make sure they are still connected // and read anything they may have output. Iterator iterator = processList.iterator(); while (iterator.hasNext()) { Process process = (Process) iterator.next(); // First, see if the process has exited. try { int exitValue = process.exitValue(); messageWriter.writeMessage("Client exited with exit code " + exitValue); iterator.remove(); continue; } catch (IllegalThreadStateException itse) {} // Read any output that may be available on the process. try { InputStream inputStream = process.getInputStream(); while (inputStream.available() > 0) { inputStream.read(readBuffer); } } catch (Exception e) { messageWriter.writeVerbose("Error reading client output: " + e); } } } } /** * Retrieves the next message ID that should be used to send a message to the * SLAMD server. * * @return The next message ID that should be used to send a message to the * SLAMD server. */ public int nextMessageID() { int idToReturn = nextMessageID; nextMessageID += 2; return idToReturn; } /** * Attempts to start the specified number of clients, presumably as a result * of a start client request from the SLAMD server. In general, this should * only be called when automatically creating client instances when the client * manager connects to the SLAMD server. * * @param numClients The number of instances of the client to create. */ public void startClients(int numClients) { if ((maxClients > 0) && ((numClients + processList.size()) > maxClients)) { messageWriter.writeMessage("Rejecting the StartClient request -- " + "insufficient clients available"); return; } Runtime runtime = Runtime.getRuntime(); for (int i=0; i < numClients; i++) { try { Process process = runtime.exec(startCommand); processList.add(process); } catch (Exception e) { messageWriter.writeMessage("Rejecting the StartClient request -- " + "unable to execute start client command"); return; } } } /** * Attempts to start the specified number of clients, presumably as a result * of a start client request from the SLAMD server. * * @param startClientRequest The start client request to be processed. */ public void startClients(StartClientRequestMessage startClientRequest) { int numClients = startClientRequest.getNumClients(); if ((maxClients > 0) && ((numClients + processList.size()) > maxClients)) { StartClientResponseMessage responseMessage = new StartClientResponseMessage(startClientRequest.getMessageID(), Constants.MESSAGE_RESPONSE_INSUFFICIENT_CLIENTS, "Insufficient clients are available to process the request."); messageWriter.writeMessage("Rejecting the StartClient request -- " + "insufficient clients available"); messageWriter.writeVerbose(responseMessage.toString()); try { asn1Writer.writeElement(responseMessage.encode()); } catch (Exception e) {} return; } Runtime runtime = Runtime.getRuntime(); for (int i=0; i < numClients; i++) { try { Process process = runtime.exec(startCommand); processList.add(process); } catch (Exception e) { StartClientResponseMessage responseMessage = new StartClientResponseMessage(startClientRequest.getMessageID(), Constants.MESSAGE_RESPONSE_LOCAL_ERROR, "Unable to execute the start client command: " + e); messageWriter.writeMessage("Rejecting the StartClient request -- " + "unable to execute start client command"); messageWriter.writeVerbose(responseMessage.toString()); try { asn1Writer.writeElement(responseMessage.encode()); } catch (Exception e2) {} return; } } StartClientResponseMessage responseMessage = new StartClientResponseMessage(startClientRequest.getMessageID(), Constants.MESSAGE_RESPONSE_SUCCESS); messageWriter.writeVerbose(responseMessage.toString()); try { asn1Writer.writeElement(responseMessage.encode()); } catch (Exception e) {} } /** * Attempts to stop the specified number of clients, presumably as a result of * a stop client request from the SLAMD server. * * @param stopClientRequest The request received from the server. */ public void stopClients(StopClientRequestMessage stopClientRequest) { int numClients; if (stopClientRequest == null) { // This should never happen. numClients = processList.size(); } else { numClients = stopClientRequest.getNumClients(); if (numClients < 0) { numClients = processList.size(); } } int numKilled = 0; Iterator iterator = processList.iterator(); while ((numKilled < numClients) && iterator.hasNext()) { Process process = (Process) iterator.next(); try { process.destroy(); } catch (Exception e) { e.printStackTrace(); } numKilled++; } if (stopClientRequest == null) { return; } StopClientResponseMessage response = new StopClientResponseMessage(stopClientRequest.getMessageID(), Constants.MESSAGE_RESPONSE_SUCCESS, numKilled + " clients stopped"); messageWriter.writeVerbose(response.toString()); try { asn1Writer.writeElement(response.encode()); } catch (Exception e) {} } /** * Closes the connection to the SLAMD server and disconnects any clients that * may be connected. */ public void disconnect() { messageWriter.writeMessage("Disconnecting from the SLAMD server"); for (int i=0; i < processList.size(); i++) { Process process = processList.get(i); try { process.destroy(); } catch (Exception e) {} } processList.clear(); try { socket.close(); } catch (Exception e2) {} connected = false; } /** * Allows the client manager to sleep for a period of time before it checks to * see if the SLAMD server is available. It will generally sleep for a time * equal to <CODE>SERVER_DOWN_CHECK_TIME</CODE>, but it can stop sooner if the * client manager is asked to shut down. */ public void sleepBeforeReconnectAttempt() { long now = System.currentTimeMillis(); long wakeUpTime = now + SERVER_DOWN_CHECK_TIME; while ((! stopClientManager) && (System.currentTimeMillis() < wakeUpTime)) { try { Thread.sleep(MAX_BLOCK_TIME); } catch (InterruptedException ie) {} } } }