aboutsummaryrefslogtreecommitdiffstats
path: root/ddms/libs/ddmlib/src/com/android/ddmlib/Debugger.java
blob: 9356c13e4891f46326d2a223e7c7e85b9cc01f04 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.ddmlib;

import com.android.ddmlib.ClientData.DebuggerStatus;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

/**
 * This represents a pending or established connection with a JDWP debugger.
 */
class Debugger {

    /*
     * Messages from the debugger should be pretty small; may not even
     * need an expanding-buffer implementation for this.
     */
    private static final int INITIAL_BUF_SIZE = 1 * 1024;
    private static final int MAX_BUF_SIZE = 32 * 1024;
    private ByteBuffer mReadBuffer;

    private static final int PRE_DATA_BUF_SIZE = 256;
    private ByteBuffer mPreDataBuffer;

    /* connection state */
    private int mConnState;
    private static final int ST_NOT_CONNECTED = 1;
    private static final int ST_AWAIT_SHAKE   = 2;
    private static final int ST_READY         = 3;

    /* peer */
    private Client mClient;         // client we're forwarding to/from
    private int mListenPort;        // listen to me
    private ServerSocketChannel mListenChannel;

    /* this goes up and down; synchronize methods that access the field */
    private SocketChannel mChannel;

    /**
     * Create a new Debugger object, configured to listen for connections
     * on a specific port.
     */
    Debugger(Client client, int listenPort) throws IOException {

        mClient = client;
        mListenPort = listenPort;

        mListenChannel = ServerSocketChannel.open();
        mListenChannel.configureBlocking(false);        // required for Selector

        InetSocketAddress addr = new InetSocketAddress(
                InetAddress.getByName("localhost"), //$NON-NLS-1$
                listenPort);
        mListenChannel.socket().setReuseAddress(true);  // enable SO_REUSEADDR
        mListenChannel.socket().bind(addr);

        mReadBuffer = ByteBuffer.allocate(INITIAL_BUF_SIZE);
        mPreDataBuffer = ByteBuffer.allocate(PRE_DATA_BUF_SIZE);
        mConnState = ST_NOT_CONNECTED;

        Log.d("ddms", "Created: " + this.toString());
    }

    /**
     * Returns "true" if a debugger is currently attached to us.
     */
    boolean isDebuggerAttached() {
        return mChannel != null;
    }

    /**
     * Represent the Debugger as a string.
     */
    @Override
    public String toString() {
        // mChannel != null means we have connection, ST_READY means it's going
        return "[Debugger " + mListenPort + "-->" + mClient.getClientData().getPid()
                + ((mConnState != ST_READY) ? " inactive]" : " active]");
    }

    /**
     * Register the debugger's listen socket with the Selector.
     */
    void registerListener(Selector sel) throws IOException {
        mListenChannel.register(sel, SelectionKey.OP_ACCEPT, this);
    }

    /**
     * Return the Client being debugged.
     */
    Client getClient() {
        return mClient;
    }

    /**
     * Accept a new connection, but only if we don't already have one.
     *
     * Must be synchronized with other uses of mChannel and mPreBuffer.
     *
     * Returns "null" if we're already talking to somebody.
     */
    synchronized SocketChannel accept() throws IOException {
        return accept(mListenChannel);
    }

    /**
     * Accept a new connection from the specified listen channel.  This
     * is so we can listen on a dedicated port for the "current" client,
     * where "current" is constantly in flux.
     *
     * Must be synchronized with other uses of mChannel and mPreBuffer.
     *
     * Returns "null" if we're already talking to somebody.
     */
    synchronized SocketChannel accept(ServerSocketChannel listenChan)
        throws IOException {

        if (listenChan != null) {
            SocketChannel newChan;

            newChan = listenChan.accept();
            if (mChannel != null) {
                Log.w("ddms", "debugger already talking to " + mClient
                    + " on " + mListenPort);
                newChan.close();
                return null;
            }
            mChannel = newChan;
            mChannel.configureBlocking(false);         // required for Selector
            mConnState = ST_AWAIT_SHAKE;
            return mChannel;
        }

        return null;
    }

    /**
     * Close the data connection only.
     */
    synchronized void closeData() {
        try {
            if (mChannel != null) {
                mChannel.close();
                mChannel = null;
                mConnState = ST_NOT_CONNECTED;

                ClientData cd = mClient.getClientData();
                cd.setDebuggerConnectionStatus(DebuggerStatus.DEFAULT);
                mClient.update(Client.CHANGE_DEBUGGER_STATUS);
            }
        } catch (IOException ioe) {
            Log.w("ddms", "Failed to close data " + this);
        }
    }

    /**
     * Close the socket that's listening for new connections and (if
     * we're connected) the debugger data socket.
     */
    synchronized void close() {
        try {
            if (mListenChannel != null) {
                mListenChannel.close();
            }
            mListenChannel = null;
            closeData();
        } catch (IOException ioe) {
            Log.w("ddms", "Failed to close listener " + this);
        }
    }

    // TODO: ?? add a finalizer that verifies the channel was closed

    /**
     * Read data from our channel.
     *
     * This is called when data is known to be available, and we don't yet
     * have a full packet in the buffer.  If the buffer is at capacity,
     * expand it.
     */
    void read() throws IOException {
        int count;

        if (mReadBuffer.position() == mReadBuffer.capacity()) {
            if (mReadBuffer.capacity() * 2 > MAX_BUF_SIZE) {
                throw new BufferOverflowException();
            }
            Log.d("ddms", "Expanding read buffer to "
                + mReadBuffer.capacity() * 2);

            ByteBuffer newBuffer =
                    ByteBuffer.allocate(mReadBuffer.capacity() * 2);
            mReadBuffer.position(0);
            newBuffer.put(mReadBuffer);     // leaves "position" at end

            mReadBuffer = newBuffer;
        }

        count = mChannel.read(mReadBuffer);
        Log.v("ddms", "Read " + count + " bytes from " + this);
        if (count < 0) throw new IOException("read failed");
    }

    /**
     * Return information for the first full JDWP packet in the buffer.
     *
     * If we don't yet have a full packet, return null.
     *
     * If we haven't yet received the JDWP handshake, we watch for it here
     * and consume it without admitting to have done so.  We also send
     * the handshake response to the debugger, along with any pending
     * pre-connection data, which is why this can throw an IOException.
     */
    JdwpPacket getJdwpPacket() throws IOException {
        /*
         * On entry, the data starts at offset 0 and ends at "position".
         * "limit" is set to the buffer capacity.
         */
        if (mConnState == ST_AWAIT_SHAKE) {
            int result;

            result = JdwpPacket.findHandshake(mReadBuffer);
            //Log.v("ddms", "findHand: " + result);
            switch (result) {
                case JdwpPacket.HANDSHAKE_GOOD:
                    Log.d("ddms", "Good handshake from debugger");
                    JdwpPacket.consumeHandshake(mReadBuffer);
                    sendHandshake();
                    mConnState = ST_READY;

                    ClientData cd = mClient.getClientData();
                    cd.setDebuggerConnectionStatus(DebuggerStatus.ATTACHED);
                    mClient.update(Client.CHANGE_DEBUGGER_STATUS);

                    // see if we have another packet in the buffer
                    return getJdwpPacket();
                case JdwpPacket.HANDSHAKE_BAD:
                    // not a debugger, throw an exception so we drop the line
                    Log.d("ddms", "Bad handshake from debugger");
                    throw new IOException("bad handshake");
                case JdwpPacket.HANDSHAKE_NOTYET:
                    break;
                default:
                    Log.e("ddms", "Unknown packet while waiting for client handshake");
            }
            return null;
        } else if (mConnState == ST_READY) {
            if (mReadBuffer.position() != 0) {
                Log.v("ddms", "Checking " + mReadBuffer.position() + " bytes");
            }
            return JdwpPacket.findPacket(mReadBuffer);
        } else {
            Log.e("ddms", "Receiving data in state = " + mConnState);
        }

        return null;
    }

    /**
     * Forward a packet to the client.
     *
     * "mClient" will never be null, though it's possible that the channel
     * in the client has closed and our send attempt will fail.
     *
     * Consumes the packet.
     */
    void forwardPacketToClient(JdwpPacket packet) throws IOException {
        mClient.sendAndConsume(packet);
    }

    /**
     * Send the handshake to the debugger.  We also send along any packets
     * we already received from the client (usually just a VM_START event,
     * if anything at all).
     */
    private synchronized void sendHandshake() throws IOException {
        ByteBuffer tempBuffer = ByteBuffer.allocate(JdwpPacket.HANDSHAKE_LEN);
        JdwpPacket.putHandshake(tempBuffer);
        int expectedLength = tempBuffer.position();
        tempBuffer.flip();
        if (mChannel.write(tempBuffer) != expectedLength) {
            throw new IOException("partial handshake write");
        }

        expectedLength = mPreDataBuffer.position();
        if (expectedLength > 0) {
            Log.d("ddms", "Sending " + mPreDataBuffer.position()
                    + " bytes of saved data");
            mPreDataBuffer.flip();
            if (mChannel.write(mPreDataBuffer) != expectedLength) {
                throw new IOException("partial pre-data write");
            }
            mPreDataBuffer.clear();
        }
    }

    /**
     * Send a packet to the debugger.
     *
     * Ideally, we can do this with a single channel write.  If that doesn't
     * happen, we have to prevent anybody else from writing to the channel
     * until this packet completes, so we synchronize on the channel.
     *
     * Another goal is to avoid unnecessary buffer copies, so we write
     * directly out of the JdwpPacket's ByteBuffer.
     *
     * We must synchronize on "mChannel" before writing to it.  We want to
     * coordinate the buffered data with mChannel creation, so this whole
     * method is synchronized.
     */
    synchronized void sendAndConsume(JdwpPacket packet)
        throws IOException {

        if (mChannel == null) {
            /*
             * Buffer this up so we can send it to the debugger when it
             * finally does connect.  This is essential because the VM_START
             * message might be telling the debugger that the VM is
             * suspended.  The alternative approach would be for us to
             * capture and interpret VM_START and send it later if we
             * didn't choose to un-suspend the VM for our own purposes.
             */
            Log.d("ddms", "Saving packet 0x"
                    + Integer.toHexString(packet.getId()));
            packet.movePacket(mPreDataBuffer);
        } else {
            packet.writeAndConsume(mChannel);
        }
    }
}