/* * Copyright 2007-2010 Sun Microsystems, Inc. * * This file is part of Project Darkstar Server. * * Project Darkstar Server is free software: you can redistribute it * and/or modify it under the terms of the GNU General Public License * version 2 as published by the Free Software Foundation and * distributed hereunder to you. * * Project Darkstar Server is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * -- */ package com.sun.sgs.test.util; import com.sun.sgs.impl.io.SocketEndpoint; import com.sun.sgs.impl.io.TransportType; import com.sun.sgs.impl.sharedutil.HexDumper; import com.sun.sgs.impl.sharedutil.MessageBuffer; import com.sun.sgs.io.Connection; import com.sun.sgs.io.ConnectionListener; import com.sun.sgs.io.Connector; import com.sun.sgs.protocol.simple.SimpleSgsProtocol; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.util.concurrent.ConcurrentLinkedQueue; import org.junit.Assert; /** * Abstract dummy client code for testing purposes. */ public abstract class AbstractDummyClient extends Assert { private static final int WAIT_TIME = 5000; private static final byte RELOCATE_PROTOCOL_VERSION = 5; /* -- client state -- */ public final String name; private final byte protocolVersion; protected volatile byte[] reconnectKey = new byte[0]; private final Object lock = new Object(); /* -- connection state -- */ private Connector<SocketAddress> connector; private Listener listener; private Connection connection; private volatile int connectPort = 0; private boolean connected = false; private boolean waitForSuspendMessages = false; private boolean suspendMessages = false; /* -- login/logout state -- */ private boolean loginAck = false; private boolean loginSuccess = false; private boolean logoutAck = false; /* -- redirection state -- */ private boolean loginRedirect = false; private String redirectHost; public int redirectPort; /* -- relocation state -- */ private boolean relocateSession; private String relocateHost; private int relocatePort; private byte[] relocateKey = new byte[0]; private boolean relocateAck; private boolean relocateSuccess; /** * Constructs an instance with the given {@code name} with the latest * protocol version. */ public AbstractDummyClient(String name) { this(name, SimpleSgsProtocol.VERSION); } /** * Constructs an instance with the given {@code name} and the specified * protocol version. */ public AbstractDummyClient(String name, byte protocolVersion) { this.name = name; this.protocolVersion = protocolVersion; } /** * Connects this client to the given {@code port} and returns this * instance. */ public AbstractDummyClient connect(int port) { connectPort = port; connected = false; listener = new Listener(); try { SocketEndpoint endpoint = new SocketEndpoint( new InetSocketAddress(InetAddress.getLocalHost(), port), TransportType.RELIABLE); connector = endpoint.createConnector(); connector.connect(listener); } catch (Exception e) { System.err.println(toString() + " connect throws: " + e); e.printStackTrace(); throw new RuntimeException("DummyClient.connect failed", e); } synchronized (lock) { try { if (connected == false) { lock.wait(WAIT_TIME); } if (connected != true) { throw new RuntimeException( toString() + " connect timed out to " + port); } } catch (InterruptedException e) { throw new RuntimeException( toString() + " connect timed out to " + port, e); } } return this; } /** * Returns {@code true} if this client is connected. */ public boolean isConnected() { synchronized (lock) { return connected; } } /** * Returns the port last specified for the {@link #connect connect} * method. */ public int getConnectPort() { return connectPort; } /** * Returns the redirect port. */ public int getRedirectPort() { synchronized (lock) { return redirectPort; } } /** * Throws a {@code RuntimeException} if this session is not * logged in. */ protected void checkLoggedIn() { synchronized (lock) { if (!(connected && (loginSuccess || relocateSuccess ))) { throw new RuntimeException( toString() + " not connected or loggedIn"); } } } /** * Sends a login request and waits for it to be acknowledged, * returning {@code true} if login was successful, and {@code * false} if login was redirected. If the login was not successful * or redirected, then a {@code RuntimeException} is thrown because * the login operation timed out before being acknowledged. */ public boolean login() { return login(true); } /** * Sends a login request and if {@code waitForLogin} is {@code * true} waits for the request to be acknowledged, returning {@code * true} if login was successful, and {@code false} if login was * redirected, otherwise a {@code RuntimeException} is thrown * because the login operation timed out before being acknowledged. * * If {@code waitForLogin} is false, this method returns {@code * true} if the login is known to be successful (the outcome may * not yet be known because the login operation is asynchronous), * otherwise it returns false. Invoke {@code waitForLogin} to wait * for an expected successful login. */ public boolean login(boolean waitForLogin) { synchronized (lock) { if (connected == false) { throw new RuntimeException(toString() + " not connected"); } } String password = "password"; MessageBuffer buf = new MessageBuffer(2 + MessageBuffer.getSize(name) + MessageBuffer.getSize(password)); buf.putByte(SimpleSgsProtocol.LOGIN_REQUEST). putByte(protocolVersion). putString(name). putString(password); loginAck = false; try { connection.sendBytes(buf.getBuffer()); } catch (IOException e) { throw new RuntimeException(e); } if (waitForLogin) { if (waitForLogin()) { return true; } else if (isLoginRedirected()) { int port = redirectPort; disconnect(); connect(port); return login(); } } synchronized (lock) { return loginSuccess; } } /** * Waits for a login acknowledgement, and returns {@code true} if * login was successful, {@code false} if login was redirected or * failed, otherwise a {@code RuntimeException} is thrown because * the login operation timed out before being acknowledged. */ public boolean waitForLogin() { synchronized (lock) { try { if (loginAck == false) { lock.wait(WAIT_TIME); } if (loginAck != true) { throw new RuntimeException(toString() + " login timed out"); } if (loginRedirect == true) { return false; } return loginSuccess; } catch (InterruptedException e) { throw new RuntimeException(toString() + " login timed out", e); } } } /** * Returns {@code true} if the login is redirected, otherwise returns * {@code false}. */ public boolean isLoginRedirected() { return loginRedirect; } /** * Notifies this client that it is logged in or relocated and a new * reconnect key has been granted. */ protected void newReconnectKey(byte[] reconnectKey) { } /** * Waits for this client to receive a RELOCATE_NOTIFICATION message. * Throws {@code AssertionFailedError} if the notification is not * received before the timeout period, or if {@code expectedPort} is * non-zero and does not match the relocation port specified in the * received RELOCATE_NOTIFICATION. */ public void waitForRelocationNotification(int expectedPort) { checkRelocateProtocolVersion(); System.err.println(toString() + " waiting for relocation notification..."); synchronized (lock) { try { if (relocateSession == false) { lock.wait(WAIT_TIME); } if (relocateSession != true) { fail(toString() + " relocate notification timed out"); } if (expectedPort != 0) { assertEquals(expectedPort, relocatePort); } } catch (InterruptedException e) { fail(toString() + " relocated timed out: " + e.toString()); } } } /** * Relocates this client's connection, if the server has instructed it * to do so via a RELOCATE_NOTIFICATION. <p> * * If this client has not yet received a RELOCATE_NOTIFICATION, it first * waits until one is received or the timeout expires, which ever comes * first. If a RELOCATE_NOTIFICATION is not received or if the * specified {@code expectedPort} is non-zero and does not match the * relocation port, then {@code AssertionFailedError} is thrown. <p> * * If a RELOCATE_NOTIFICATION is correctly received, then this method * sends a RELOCATE_REQUEST message to the local host on the relocation * port received by a RELOCATE_NOTIFICATION. If {@code useValidKey} is * {@code true}, the current valid relocation key is used in the * relocate request, otherwise an invalid relocation key is used. <p> * * This method waits for an acknowledgment (either RELOCATE_SUCCESS or * RELOCATE_FAILURE). If {@code shouldSucceed} is {@code true} and a * RELOCATE_FAILURE is received, this method throws {@code * AssertionFailedError}; similarly if {@code shouldSucceed} is {@code * false} and a RELOCATE_SUCCESS is received, {@code * AssertionFailedError} will be thrown. */ public void relocate(int expectedPort, boolean useValidKey, boolean shouldSucceed) { checkRelocateProtocolVersion(); synchronized (lock) { if (!relocateSession) { waitForRelocationNotification(expectedPort); } else { if (expectedPort != 0) { assertEquals(expectedPort, relocatePort); } } } System.err.println(toString() + " relocating..."); disconnect(); relocateAck = false; relocateSuccess = false; suspendMessages = false; connect(relocatePort); byte[] key = useValidKey ? relocateKey : new byte[0]; ByteBuffer buf = ByteBuffer.allocate(2 + key.length); buf.put(SimpleSgsProtocol.RELOCATE_REQUEST). put(SimpleSgsProtocol.VERSION). put(key). flip(); try { connection.sendBytes(buf.array()); } catch (IOException e) { throw new RuntimeException(e); } synchronized (lock) { try { if (!relocateAck) { lock.wait(WAIT_TIME); } if (!relocateAck) { throw new RuntimeException( toString() + " relocate timed out"); } if (shouldSucceed) { if (!relocateSuccess) { fail("relocation failed"); } } else if (relocateSuccess) { fail("relocation succeeded"); } } catch (InterruptedException e) { throw new RuntimeException( toString() + " relocate timed out", e); } relocateSession = false; relocateAck = false; relocatePort = 0; relocateSuccess = false; } } /** * Sends a SESSION_MESSAGE to the server with the specified content. * If {@code checkSuspend} is {@code true}, then, if the subclass * supports suspending messages, it should check to see if messages are * suspended before forwarding the message to the server. The method * default implementation of {@link #sendRaw} will check if messages * are suspended. */ public abstract void sendMessage(byte[] message, boolean checkSuspend); /** * Writes the specified {@code bytes} directly to the underlying * connection. If {@code checkSuspended} is {@code true}, and messages * sending is currently suspended, then throw an {@code * IllegalStateException}. */ protected void sendRaw(byte[] bytes, boolean checkSuspended) { synchronized (lock) { if (checkSuspended && suspendMessages) { throw new IllegalStateException("messages suspended"); } try { connection.sendBytes(bytes); } catch (IOException e) { throw new RuntimeException(e); } } } /** * If this session is not connected, this method returns; otherwise * this method sends a LOGOUT_REQUEST to the server, and waits for * this client to receive a LOGOUT_SUCCESS acknowledgment or the * timeout to expire, whichever comes first. If the LOGOUT_SUCCESS * acknowledgment is not received, then {@code AssertionFailedError} * is thrown. */ public void logout() { synchronized (lock) { if (connected == false) { return; } logoutAck = false; } MessageBuffer buf = new MessageBuffer(1); buf.putByte(SimpleSgsProtocol.LOGOUT_REQUEST); try { connection.sendBytes(buf.getBuffer()); } catch (IOException e) { throw new RuntimeException(e); } synchronized (lock) { try { if (logoutAck == false) { lock.wait(WAIT_TIME); } if (logoutAck != true) { fail(toString() + " logout timed out"); } } catch (InterruptedException e) { fail(toString() + " logout timed out: " + e.toString()); } finally { if (! logoutAck) disconnect(); } } } /** * Disconnects this client, and returns either when the connection * is closed or the timeout expires, which ever comes first. */ public void disconnect() { System.err.println(toString() + " disconnecting"); synchronized (lock) { if (! connected) { return; } try { connection.close(); lock.wait(WAIT_TIME); } catch (Exception e) { System.err.println(toString() + " disconnect exception:" + e); lock.notifyAll(); } finally { if (connected) { reset(); } } } } /** * Resets the connection state so that the client can connect and * login again. */ private void reset() { assert Thread.holdsLock(lock); connected = false; connection = null; loginAck = false; loginSuccess = false; loginRedirect = false; } /** * Waits for the connection to close or the timeout to expire, * whichever comes first, and returns {@code true} if the * underlying connection disconnected, and {@code false} otherwise. */ public boolean waitForDisconnect() { synchronized (lock) { try { if (connected) { lock.wait(WAIT_TIME); } } catch (InterruptedException ignore) { } return !connected; } } /** * Sets a flag that indicates this client should wait for a * SUSPEND_MESSAGES notification, instead of replying with a * SUSPEND_MESSAGES_COMPLETE ack to the server. To wait for the * SUSPEND_MESSAGES notification, invoke the {@link * #waitForSuspendMessages} method. */ public void setWaitForSuspendMessages() { checkRelocateProtocolVersion(); synchronized (lock) { waitForSuspendMessages = true; } } /** * Waits for a SUSPEND_MESSAGES notification to be sent to this * client. A previous call to {@link #setWaitForSuspendMessages} must * be invoked before the server sends a SUSPEND_MESSAGES notification * in order for this wait to be successful. This method returns when a * SUSPEND_MESSAGES notification has been received; it does NOT allow a * SUSPEND_MESSAGES_COMPLETE ack to be sent. To send the * SUSPEND_MESSAGES_COMPLETE ack, invoke the {@link * #sendSuspendMessagesComplete} method. */ public void waitForSuspendMessages() { checkRelocateProtocolVersion(); synchronized (lock) { try { if (!suspendMessages) { lock.wait(WAIT_TIME); } } catch (InterruptedException ignore) { } if (!suspendMessages) { fail(toString() + " time out waiting for SUSPEND_MESSAGES"); } } } /** * Sends a SUSPEND_MESSAGES_COMPLETE ack to the server. */ public void sendSuspendMessagesComplete() { checkRelocateProtocolVersion(); synchronized (lock) { ByteBuffer msg = ByteBuffer.allocate(1); msg.put(SimpleSgsProtocol.SUSPEND_MESSAGES_COMPLETE). flip(); try { connection.sendBytes(msg.array()); } catch (IOException e) { throw new RuntimeException(e); } } } /** * Gives a subclass a chance to handle an {@code opcode}. This * implementation handles login, redirection, relocation, and * logout but does not handle session or channel messages. */ protected void handleOpCode(byte opcode, MessageBuffer buf) { switch (opcode) { case SimpleSgsProtocol.LOGIN_SUCCESS: reconnectKey = buf.getBytes(buf.limit() - buf.position()); newReconnectKey(reconnectKey); synchronized (lock) { loginAck = true; loginSuccess = true; System.err.println("login succeeded: " + name); lock.notifyAll(); } sendMessage(new byte[0], true); break; case SimpleSgsProtocol.LOGIN_FAILURE: String failureReason = buf.getString(); synchronized (lock) { loginAck = true; loginSuccess = false; System.err.println("login failed: " + name + ", reason:" + failureReason); lock.notifyAll(); } break; case SimpleSgsProtocol.LOGIN_REDIRECT: redirectHost = buf.getString(); redirectPort = buf.getInt(); synchronized (lock) { loginAck = true; loginRedirect = true; System.err.println("login redirected: " + name + ", host:" + redirectHost + ", port:" + redirectPort); lock.notifyAll(); } break; case SimpleSgsProtocol.SUSPEND_MESSAGES: checkRelocateProtocolVersion(); synchronized (lock) { if (suspendMessages) { break; } suspendMessages = true; if (waitForSuspendMessages) { waitForSuspendMessages = false; lock.notifyAll(); } else { sendSuspendMessagesComplete(); } } break; case SimpleSgsProtocol.RELOCATE_NOTIFICATION: checkRelocateProtocolVersion(); relocateHost = buf.getString(); relocatePort = buf.getInt(); relocateKey = buf.getBytes(buf.limit() - buf.position()); synchronized (lock) { relocateSession = true; System.err.println( "session to relocate: " + name + ", host:" + relocateHost + ", port:" + relocatePort + ", key:" + HexDumper.toHexString(relocateKey)); lock.notifyAll(); } break; case SimpleSgsProtocol.RELOCATE_SUCCESS: checkRelocateProtocolVersion(); reconnectKey = buf.getBytes(buf.limit() - buf.position()); newReconnectKey(reconnectKey); synchronized (lock) { relocateAck = true; relocateSuccess = true; System.err.println("relocate succeeded: " + name); lock.notifyAll(); } sendMessage(new byte[0], true); break; case SimpleSgsProtocol.RELOCATE_FAILURE: checkRelocateProtocolVersion(); String relocateFailureReason = buf.getString(); synchronized (lock) { relocateAck = true; relocateSuccess = false; System.err.println("relocate failed: " + name + ", reason:" + relocateFailureReason); lock.notifyAll(); } break; case SimpleSgsProtocol.LOGOUT_SUCCESS: synchronized (lock) { logoutAck = true; System.err.println("logout succeeded: " + name); lock.notifyAll(); } break; default: System.err.println( "WARNING: [" + name + "] dropping unknown op code: " + String.format("%02x", opcode)); break; } } /** {@inheritDoc} */ public String toString() { return "[" + name + "]"; } /** * Throws an {@code AssertionFailedError} if this client's protocol * version does not support suspend or relocate. */ public void checkRelocateProtocolVersion() { if (protocolVersion < RELOCATE_PROTOCOL_VERSION) { String badVersion = toString() + " Protocol version: " + protocolVersion + " does not support suspend or relocate"; System.err.println(badVersion); fail(badVersion); } } /** * ConnectionListener for connection I/O events. */ private class Listener implements ConnectionListener { /** {@inheritDoc} */ public void bytesReceived(Connection conn, byte[] buffer) { if (connection != conn) { System.err.println( toString() + "AbstractDummyClient.Listener.bytesReceived:" + " wrong handle, got:" + conn + ", expected:" + connection); return; } MessageBuffer buf = new MessageBuffer(buffer); byte opcode = buf.getByte(); handleOpCode(opcode, buf); } /** {@inheritDoc} */ public void connected(Connection conn) { System.err.println( AbstractDummyClient.this.toString() + " connected to port:" + connectPort); if (connection != null) { System.err.println( "DummyClient.Listener.already connected handle: " + connection); return; } connection = conn; synchronized (lock) { connected = true; lock.notifyAll(); } } /** {@inheritDoc} */ public void disconnected(Connection conn) { synchronized (lock) { reset(); lock.notifyAll(); } } /** {@inheritDoc} */ public void exceptionThrown(Connection conn, Throwable exception) { System.err.println("DummyClient.Listener.exceptionThrown " + "exception:" + exception); exception.printStackTrace(); } } }