// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.sdk.internal.websocket; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetEncoder; import java.nio.charset.CoderResult; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.logging.Level; import java.util.logging.Logger; import org.chromium.sdk.ConnectionLogger; import org.chromium.sdk.RelayOk; import org.chromium.sdk.SyncCallback; import org.chromium.sdk.internal.transport.AbstractSocketWrapper; import org.chromium.sdk.util.SignalRelay; import org.chromium.sdk.util.SignalRelay.AlreadySignalledException; import org.chromium.sdk.util.SignalRelay.SignalConverter; public abstract class AbstractWsConnection<INPUT, OUTPUT> implements WsConnection { protected static final Charset UTF_8_CHARSET = Charset.forName("UTF-8"); private static final Logger LOGGER = Logger.getLogger(Hybi00WsConnection.class.getName()); private final AbstractSocketWrapper<INPUT, OUTPUT> socketWrapper; private final ConnectionLogger connectionLogger; private volatile boolean isClosingGracefully = false; private final BlockingQueue<MessageDispatcher> dispatchQueue = new LinkedBlockingQueue<MessageDispatcher>(); // Access must be synchronized on dispatchQueue. private boolean isDispatchQueueClosed = false; // Access must be synchronized on this. private boolean isOutputClosed = false; protected AbstractWsConnection(AbstractSocketWrapper<INPUT, OUTPUT> socketWrapper, ConnectionLogger connectionLogger) { this.socketWrapper = socketWrapper; this.connectionLogger = connectionLogger; try { linkedCloser.bind(socketWrapper.getShutdownRelay(), null, SOCKET_TO_CONNECTION); } catch (AlreadySignalledException e) { throw new IllegalStateException(e); } } public enum CloseReason { /** Socket has been shut down. */ CONNECTION_CLOSED, /** * Some exception has terminated stream read thread. * Occasionally {@link #CONNECTION_CLOSED} may be replaced with this reason (we are not * accurate enough here). */ INPUT_STREAM_PROBLEM, /** * Closed as requested by {@link WsConnection#close()}. */ USER_REQUEST, /** * Connection close has been requested from remote side. */ REMOTE_CLOSE_REQUEST } @Override public RelayOk runInDispatchThread(final Runnable runnable, final SyncCallback syncCallback) { MessageDispatcher messageDispatcher = new MessageDispatcher() { @Override boolean dispatch(Listener userListener) { RuntimeException ex = null; try { runnable.run(); } catch (RuntimeException e) { ex = e; throw e; } finally { syncCallback.callbackDone(ex); } return false; } }; synchronized (dispatchQueue) { if (isDispatchQueueClosed) { throw new IllegalStateException("Connection is closed"); } dispatchQueue.add(messageDispatcher); } return DISPATCH_THREAD_PROMISES_TO_RELAY_OK; } @Override public void startListening(final Listener listener) { final INPUT loggableReader = socketWrapper.getLoggableInput(); Runnable listenRunnable = new Runnable() { @Override public void run() { Exception closeCause = null; CloseReason closeReason = null; try { closeReason = runListenLoop(loggableReader); } catch (IOException e) { closeCause = e; LOGGER.log(Level.SEVERE, "Connection read failure", e); } catch (InterruptedException e) { closeCause = e; closeReason = CloseReason.USER_REQUEST; LOGGER.log(Level.SEVERE, "Thread interruption", e); } finally { synchronized (dispatchQueue) { dispatchQueue.add(EOS_MESSAGE_DISPATCHER); isDispatchQueueClosed = true; } if (connectionLogger != null) { connectionLogger.handleEos(); } if (closeReason == null) { closeReason = CloseReason.INPUT_STREAM_PROBLEM; } linkedCloser.sendSignal(closeReason, closeCause); } } }; Thread readThread = new Thread(listenRunnable, "WebSocket listen thread"); readThread.setDaemon(true); readThread.start(); if (connectionLogger != null) { connectionLogger.start(); } Runnable dispatchRunnable = new Runnable() { @Override public void run() { try { runImpl(); } catch (InterruptedException e) { LOGGER.log(Level.SEVERE, "Thread interruption", e); } } private void runImpl() throws InterruptedException { while (true) { MessageDispatcher next = dispatchQueue.take(); try { boolean isLast = next.dispatch(listener); if (isLast) { return; } } catch (RuntimeException e) { LOGGER.log(Level.SEVERE, "Exception in dispatch thread", e); } } } }; Thread dispatchThread = new Thread(dispatchRunnable, "WebSocket dispatch thread"); dispatchThread.setDaemon(true); dispatchThread.start(); } @Override public abstract void sendTextualMessage(String message) throws IOException; protected abstract CloseReason runListenLoop(INPUT loggableReader) throws IOException, InterruptedException; public SignalRelay<?> getCloser() { return linkedCloser; } protected AbstractSocketWrapper<INPUT, OUTPUT> getSocketWrapper() { return socketWrapper; } protected boolean isClosingGracefully() { return isClosingGracefully; } /** * Caller must be synchronized on this. */ protected boolean isOutputClosed() { return isOutputClosed; } /** * Caller must be synchronized on this. */ protected void setOutputClosed(boolean isOutputClosed) { this.isOutputClosed = isOutputClosed; } protected BlockingQueue<MessageDispatcher> getDispatchQueue() { return dispatchQueue; } private final SignalRelay<CloseReason> linkedCloser = SignalRelay.create(new SignalRelay.Callback<CloseReason>() { @Override public void onSignal(CloseReason param, Exception cause) { isClosingGracefully = true; } }); /** * A debug charset that simply encodes all non-ascii symbols as %DDD. * We need it for log console because web-socket connection is essentially a random * sequence of bytes. */ protected static final Charset LOGGER_CHARSET = new Charset("Chromium_Logger_Charset", new String[0]) { @Override public boolean contains(Charset cs) { return this == cs; } @Override public CharsetDecoder newDecoder() { return new CharsetDecoder(this, 4 / 2, 4) { @Override protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) { while (in.hasRemaining()) { byte b = in.get(); if (b < 20 && b != (byte) '\n') { if (out.remaining() < 4) { return CoderResult.OVERFLOW; } out.put('%'); int code = b; int d1 = code / 100 % 10; int d2 = code / 10 % 10; int d3 = code % 10; out.put((char) ('0' + d1)); out.put((char) ('0' + d2)); out.put((char) ('0' + d3)); } else { char ch = (char) b; if (ch == '%') { if (out.remaining() < 2) { return CoderResult.OVERFLOW; } out.put('%'); out.put('%'); } else { if (!out.hasRemaining()) { return CoderResult.OVERFLOW; } out.put(ch); } } } return CoderResult.UNDERFLOW; } }; } @Override public CharsetEncoder newEncoder() { throw new UnsupportedOperationException(); } }; protected static void dumpByte(byte b, StringBuilder output) { output.append('%'); int code = (b + 256) % 256; int d1 = code / 100 % 10; int d2 = code / 10 % 10; int d3 = code % 10; output.append((char) ('0' + d1)); output.append((char) ('0' + d2)); output.append((char) ('0' + d3)); } static abstract class MessageDispatcher { /** * Dispatches message to user. * @return true if it was a last message in queue */ abstract boolean dispatch(Listener userListener); } private static final MessageDispatcher EOS_MESSAGE_DISPATCHER = new MessageDispatcher() { @Override boolean dispatch(Listener userListener) { userListener.eofMessage(); return true; } }; private static final SignalConverter<AbstractSocketWrapper.ShutdownSignal, CloseReason> SOCKET_TO_CONNECTION = new SignalConverter<AbstractSocketWrapper.ShutdownSignal, CloseReason>() { @Override public CloseReason convert(AbstractSocketWrapper.ShutdownSignal source) { return CloseReason.CONNECTION_CLOSED; } }; private static final RelayOk DISPATCH_THREAD_PROMISES_TO_RELAY_OK = new RelayOk() {}; }