• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
4 
5 package org.chromium.components.devtools_bridge;
6 
7 import android.net.LocalServerSocket;
8 import android.net.LocalSocket;
9 import android.util.Log;
10 
11 import java.io.IOException;
12 import java.util.HashMap;
13 import java.util.Map;
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;
19 
20 /**
21  * Listens LocalServerSocket and tunnels all connections to the SocketTunnelServer.
22  */
23 public class SocketTunnelClient extends SocketTunnelBase {
24     private static final String TAG = "SocketTunnelClient";
25 
26     private enum State {
27         INITIAL, RUNNING, STOPPED
28     }
29 
30     private final AtomicReference<State> mState = new AtomicReference<State>(State.INITIAL);
31 
32     private final LocalServerSocket mSocket;
33     private final ExecutorService mThreadPool = Executors.newCachedThreadPool();
34 
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>();
38 
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>();
44 
45     private final IdRegistry mIdRegistry = new IdRegistry(MIN_CONNECTION_ID, MAX_CONNECTION_ID, 2);
46 
47     /**
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.
51      */
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();
58 
IdRegistry(int minId, int maxId, int maxLocks)59         public IdRegistry(int minId, int maxId, int maxLocks) {
60             assert minId < maxId;
61             assert maxLocks > 0;
62 
63             mMin = minId;
64             mMax = maxId;
65             mMaxLocks = maxLocks;
66             mLocks = new int[maxId - minId + 1];
67         }
68 
lock(int id)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();
74                 }
75                 mLocks[index]++;
76             }
77         }
78 
release(int id)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));
84                 }
85                 mLocks[index]--;
86             }
87         }
88 
isLocked(int id)89         public boolean isLocked(int id) {
90             synchronized (mLock) {
91                 return mLocks[toIndex(id)] > 0;
92             }
93         }
94 
generate()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) {
100                         mLocks[index] = 1;
101                         return id;
102                     }
103                 }
104             }
105             throw new NoIdAvailableException();
106         }
107 
toIndex(int id)108         private int toIndex(int id) {
109             if (id < mMin || id > mMax) {
110                 throw new RuntimeException();
111             }
112             return id - mMin;
113         }
114     }
115 
116     private static class NoIdAvailableException extends Exception {}
117 
SocketTunnelClient(String socketName)118     public SocketTunnelClient(String socketName) throws IOException {
119         mSocket = new LocalServerSocket(socketName);
120     }
121 
hasConnections()122     public boolean hasConnections() {
123         return mServerConnections.size() + mPendingConnections.size() > 0;
124     }
125 
126     @Override
unbind()127     public AbstractDataChannel unbind() {
128         AbstractDataChannel dataChannel = super.unbind();
129         close();
130         return dataChannel;
131     }
132 
close()133     public void close() {
134         if (mState.get() != State.STOPPED) closeSocket();
135     }
136 
137     @Override
onReceivedDataPacket(int connectionId, byte[] data)138     protected void onReceivedDataPacket(int connectionId, byte[] data) throws ProtocolError {
139         checkCalledOnSignalingThread();
140 
141         if (!mServerConnections.containsKey(connectionId))
142             throw new ProtocolError("Unknows connection id");
143 
144         mServerConnections.get(connectionId).onReceivedDataPacket(data);
145     }
146 
147     @Override
onReceivedControlPacket(int connectionId, byte opCode)148     protected void onReceivedControlPacket(int connectionId, byte opCode) throws ProtocolError {
149         switch (opCode) {
150             case SERVER_OPEN_ACK:
151                 onServerOpenAck(connectionId);
152                 break;
153 
154             case SERVER_CLOSE:
155                 onServerClose(connectionId);
156                 break;
157 
158             default:
159                 throw new ProtocolError("Invalid opCode");
160         }
161     }
162 
onServerOpenAck(int connectionId)163     private void onServerOpenAck(int connectionId) throws ProtocolError {
164         checkCalledOnSignalingThread();
165 
166         if (mServerConnections.containsKey(connectionId)) {
167             throw new ProtocolError("Connection already acknowledged");
168         }
169 
170         if (!mPendingConnections.containsKey(connectionId)) {
171             throw new ProtocolError("Unknow connection id");
172         }
173 
174         // Check/get is safe since it can be only removed on this thread.
175         Connection connection = mPendingConnections.get(connectionId);
176         mPendingConnections.remove(connectionId);
177 
178         mServerConnections.put(connectionId, connection);
179 
180         // Lock for client to server stream.
181         mIdRegistry.lock(connectionId);
182         mThreadPool.execute(connection);
183     }
184 
onServerClose(int connectionId)185     private void onServerClose(int connectionId) throws ProtocolError {
186         checkCalledOnSignalingThread();
187 
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.
199         } else {
200             throw new ProtocolError("Closing unknown connection");
201         }
202     }
203 
204     @Override
onDataChannelOpened()205     protected void onDataChannelOpened() {
206         if (!mState.compareAndSet(State.INITIAL, State.RUNNING)) {
207             throw new InvalidStateException();
208         }
209 
210         mThreadPool.execute(new Runnable() {
211             @Override
212             public void run() {
213                 runListenLoop();
214             }
215         });
216     }
217 
218     @Override
onDataChannelClosed()219     protected void onDataChannelClosed() {
220         // All new connections will be rejected.
221         if (!mState.compareAndSet(State.RUNNING, State.STOPPED)) {
222             throw new InvalidStateException();
223         }
224 
225         for (Connection connection : mServerConnections.values()) {
226             connection.terminate();
227         }
228 
229         for (Connection connection : mPendingConnections.values()) {
230             connection.terminate();
231         }
232 
233         closeSocket();
234 
235         mThreadPool.shutdown();
236     }
237 
closeSocket()238     private void closeSocket() {
239         try {
240             mSocket.close();
241         } catch (IOException e) {
242             Log.d(TAG, "Failed to close socket: " + e);
243             onSocketException(e, -1);
244         }
245     }
246 
runListenLoop()247     private void runListenLoop() {
248         try {
249             while (true) {
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);
255                 } else {
256                     socket.close();
257                 }
258             }
259         } catch (IOException e) {
260             if (mState.get() != State.RUNNING) {
261                 onSocketException(e, -1);
262             }
263             // Else exception expected (socket closed).
264         }
265     }
266 
clientOpenConnection(LocalSocket socket)267     private void clientOpenConnection(LocalSocket socket) throws IOException {
268         try {
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) {
274             socket.close();
275         }
276     }
277 
278     private final class Connection extends ConnectionBase implements Runnable {
Connection(int id, LocalSocket socket)279         public Connection(int id, LocalSocket socket) {
280             super(id, socket);
281         }
282 
closedByServer()283         public void closedByServer() {
284             shutdownOutput();
285         }
286 
287         @Override
run()288         public void run() {
289             assert mIdRegistry.isLocked(mId);
290 
291             runReadingLoop();
292 
293             shutdownInput();
294             sendToDataChannel(buildControlPacket(mId, CLIENT_CLOSE));
295             mIdRegistry.release(mId);  // Unlock for client to server stream.
296         }
297     }
298 
299     /**
300      * Method called in inappropriate state.
301      */
302     public static class InvalidStateException extends RuntimeException {}
303 }
304