1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.chromium.components.devtools_bridge;
7 import android.net.LocalServerSocket;
8 import android.net.LocalSocket;
9 import android.util.Log;
11 import java.io.IOException;
12 import java.util.HashMap;
14 import java.util.concurrent.ConcurrentHashMap;
15 import java.util.concurrent.ConcurrentMap;
16 import java.util.concurrent.ExecutorService;
17 import java.util.concurrent.Executors;
18 import java.util.concurrent.atomic.AtomicReference;
21 * Listens LocalServerSocket and tunnels all connections to the SocketTunnelServer.
23 public class SocketTunnelClient extends SocketTunnelBase {
24 private static final String TAG = "SocketTunnelClient";
27 INITIAL, RUNNING, STOPPED
30 private final AtomicReference<State> mState = new AtomicReference<State>(State.INITIAL);
32 private final LocalServerSocket mSocket;
33 private final ExecutorService mThreadPool = Executors.newCachedThreadPool();
35 // Connections with opened server to client stream. Always accesses on signaling thread.
36 private final Map<Integer, Connection> mServerConnections =
37 new HashMap<Integer, Connection>();
39 // Accepted connections are kept here until server returns SERVER_OPEN_ACK or SERVER_CLOSE.
40 // New connections are added in the listening loop, checked and removed on signaling thread.
41 // So add/read/remove synchronized through message round trip.
42 private final ConcurrentMap<Integer, Connection> mPendingConnections =
43 new ConcurrentHashMap<Integer, Connection>();
45 private final IdRegistry mIdRegistry = new IdRegistry(MIN_CONNECTION_ID, MAX_CONNECTION_ID, 2);
48 * This class responsible for generating valid connection IDs. It count usage of connection:
49 * one user for client to server stream and one for server to client one. When both are closed
50 * it's safe to reuse ID.
52 private static final class IdRegistry {
53 private final int[] mLocks;
54 private final int mMin;
55 private final int mMax;
56 private final int mMaxLocks;
57 private final Object mLock = new Object();
59 public IdRegistry(int minId, int maxId, int maxLocks) {
66 mLocks = new int[maxId - minId + 1];
69 public void lock(int id) {
70 synchronized (mLock) {
71 int index = toIndex(id);
72 if (mLocks[index] == 0 || mLocks[index] == mMaxLocks) {
73 throw new RuntimeException();
79 public void release(int id) {
80 synchronized (mLock) {
81 int index = toIndex(id);
82 if (mLocks[index] == 0) {
83 throw new RuntimeException("Releasing unlocked id " + Integer.toString(id));
89 public boolean isLocked(int id) {
90 synchronized (mLock) {
91 return mLocks[toIndex(id)] > 0;
95 public int generate() throws NoIdAvailableException {
96 synchronized (mLock) {
97 for (int id = mMin; id != mMax; id++) {
98 int index = toIndex(id);
99 if (mLocks[index] == 0) {
105 throw new NoIdAvailableException();
108 private int toIndex(int id) {
109 if (id < mMin || id > mMax) {
110 throw new RuntimeException();
116 private static class NoIdAvailableException extends Exception {}
118 public SocketTunnelClient(String socketName) throws IOException {
119 mSocket = new LocalServerSocket(socketName);
122 public boolean hasConnections() {
123 return mServerConnections.size() + mPendingConnections.size() > 0;
127 public AbstractDataChannel unbind() {
128 AbstractDataChannel dataChannel = super.unbind();
133 public void close() {
134 if (mState.get() != State.STOPPED) closeSocket();
138 protected void onReceivedDataPacket(int connectionId, byte[] data) throws ProtocolError {
139 checkCalledOnSignalingThread();
141 if (!mServerConnections.containsKey(connectionId))
142 throw new ProtocolError("Unknows connection id");
144 mServerConnections.get(connectionId).onReceivedDataPacket(data);
148 protected void onReceivedControlPacket(int connectionId, byte opCode) throws ProtocolError {
150 case SERVER_OPEN_ACK:
151 onServerOpenAck(connectionId);
155 onServerClose(connectionId);
159 throw new ProtocolError("Invalid opCode");
163 private void onServerOpenAck(int connectionId) throws ProtocolError {
164 checkCalledOnSignalingThread();
166 if (mServerConnections.containsKey(connectionId)) {
167 throw new ProtocolError("Connection already acknowledged");
170 if (!mPendingConnections.containsKey(connectionId)) {
171 throw new ProtocolError("Unknow connection id");
174 // Check/get is safe since it can be only removed on this thread.
175 Connection connection = mPendingConnections.get(connectionId);
176 mPendingConnections.remove(connectionId);
178 mServerConnections.put(connectionId, connection);
180 // Lock for client to server stream.
181 mIdRegistry.lock(connectionId);
182 mThreadPool.execute(connection);
185 private void onServerClose(int connectionId) throws ProtocolError {
186 checkCalledOnSignalingThread();
188 if (mServerConnections.containsKey(connectionId)) {
189 Connection connection = mServerConnections.get(connectionId);
190 mServerConnections.remove(connectionId);
191 mIdRegistry.release(connectionId); // Release sever to client stream.
192 connection.closedByServer();
193 } else if (mPendingConnections.containsKey(connectionId)) {
194 Connection connection = mPendingConnections.get(connectionId);
195 mPendingConnections.remove(connectionId);
196 connection.closedByServer();
197 sendToDataChannel(buildControlPacket(connectionId, CLIENT_CLOSE));
198 mIdRegistry.release(connectionId); // Release sever to client stream.
200 throw new ProtocolError("Closing unknown connection");
205 protected void onDataChannelOpened() {
206 if (!mState.compareAndSet(State.INITIAL, State.RUNNING)) {
207 throw new InvalidStateException();
210 mThreadPool.execute(new Runnable() {
219 protected void onDataChannelClosed() {
220 // All new connections will be rejected.
221 if (!mState.compareAndSet(State.RUNNING, State.STOPPED)) {
222 throw new InvalidStateException();
225 for (Connection connection : mServerConnections.values()) {
226 connection.terminate();
229 for (Connection connection : mPendingConnections.values()) {
230 connection.terminate();
235 mThreadPool.shutdown();
238 private void closeSocket() {
241 } catch (IOException e) {
242 Log.d(TAG, "Failed to close socket: " + e);
243 onSocketException(e, -1);
247 private void runListenLoop() {
250 LocalSocket socket = mSocket.accept();
251 State state = mState.get();
252 if (mState.get() == State.RUNNING) {
253 // Make sure no socket processed when stopped.
254 clientOpenConnection(socket);
259 } catch (IOException e) {
260 if (mState.get() != State.RUNNING) {
261 onSocketException(e, -1);
263 // Else exception expected (socket closed).
267 private void clientOpenConnection(LocalSocket socket) throws IOException {
269 int id = mIdRegistry.generate(); // id generated locked for server to client stream.
270 Connection connection = new Connection(id, socket);
271 mPendingConnections.put(id, connection);
272 sendToDataChannel(buildControlPacket(id, CLIENT_OPEN));
273 } catch (NoIdAvailableException e) {
278 private final class Connection extends ConnectionBase implements Runnable {
279 public Connection(int id, LocalSocket socket) {
283 public void closedByServer() {
289 assert mIdRegistry.isLocked(mId);
294 sendToDataChannel(buildControlPacket(mId, CLIENT_CLOSE));
295 mIdRegistry.release(mId); // Unlock for client to server stream.
300 * Method called in inappropriate state.
302 public static class InvalidStateException extends RuntimeException {}