• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.jdwptunnel.cts;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertNotNull;
21 import static org.junit.Assert.assertNull;
22 import static org.junit.Assert.assertTrue;
23 import static org.junit.Assert.fail;
24 
25 import com.android.tradefed.device.DeviceNotAvailableException;
26 import com.android.tradefed.device.ITestDevice;
27 import com.android.tradefed.log.LogUtil.CLog;
28 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
29 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
30 import com.android.tradefed.util.AbiUtils;
31 import com.android.tradefed.util.RunUtil;
32 
33 import com.sun.jdi.Bootstrap;
34 import com.sun.jdi.ReferenceType;
35 import com.sun.jdi.ThreadReference;
36 import com.sun.jdi.VirtualMachine;
37 import com.sun.jdi.VirtualMachineManager;
38 import com.sun.jdi.connect.AttachingConnector;
39 import com.sun.jdi.connect.Connector;
40 import com.sun.jdi.event.ClassPrepareEvent;
41 import com.sun.jdi.request.BreakpointRequest;
42 import com.sun.jdi.request.ClassPrepareRequest;
43 import com.sun.jdi.request.EventRequest;
44 import com.sun.jdi.request.EventRequestManager;
45 
46 import org.junit.Assume;
47 import org.junit.Before;
48 import org.junit.Test;
49 import org.junit.runner.RunWith;
50 
51 import java.io.InputStream;
52 import java.io.OutputStream;
53 import java.net.Socket;
54 import java.time.Instant;
55 import java.util.Map;
56 
57 
58 /**
59  * Host-side tests for setting up a JDWP connection to an app.
60  *
61  * <p>This test ensures that it is possible to attach a debugger to an app using 'adb' and perform
62  * at least some basic debugging actions.
63  *
64  * <p>The {@link DebuggableSampleDeviceActivity} is the activity we are debugging.
65  *
66  * <p>We will start that activity with 'wait-for-debugger', set a breakpoint on the first line of
67  * the {@code onCreate} method and wait for the breakpoint to be hit.
68  *
69  * <p>Run with: atest CtsJdwpTunnelHostTestCases
70  */
71 @RunWith(DeviceJUnit4ClassRunner.class)
72 public class JdwpTunnelTest extends BaseHostJUnit4Test {
73     private static final String DEBUGGABLE_TEST_APP_PACKAGE_NAME =
74             "android.jdwptunnel.sampleapp.debuggable";
75     private static final String DEBUGGABLE_TEST_APP_ACTIVITY_CLASS_NAME =
76             "DebuggableSampleDeviceActivity";
77     private static final String PROFILEABLE_TEST_APP_PACKAGE_NAME =
78             "android.jdwptunnel.sampleapp.profileable";
79     private static final String PROFILEABLE_TEST_APP_ACTIVITY_CLASS_NAME =
80             "ProfileableSampleDeviceActivity";
81     private static final String DDMS_TEST_APP_PACKAGE_NAME = "android.jdwptunnel.sampleapp.ddms";
82     private static final String DDMS_TEST_APP_ACTIVITY_CLASS_NAME = "DdmsSampleDeviceActivity";
83 
84     private ITestDevice mDevice;
85 
86     @Before
setUp()87     public void setUp() throws Exception {
88         installPackage("CtsJdwpTunnelDebuggableSampleApp.apk");
89         installPackage("CtsJdwpTunnelProfileableSampleApp.apk");
90         installPackage("CtsJdwpTunnelDdmsSampleApp.apk");
91         mDevice = getDevice();
92     }
93 
moveToHomeScreen()94     private void moveToHomeScreen() throws Exception {
95         // Wakeup the device if it is on the lockscreen and move it to the home screen.
96         mDevice.executeShellCommand("input keyevent KEYCODE_WAKEUP");
97         mDevice.executeShellCommand("wm dismiss-keyguard");
98         mDevice.executeShellCommand("input keyevent KEYCODE_HOME");
99     }
100 
getDebuggerConnection(String port)101     private VirtualMachine getDebuggerConnection(String port) throws Exception {
102         VirtualMachineManager vmm = Bootstrap.virtualMachineManager();
103         AttachingConnector conn =
104                 vmm.attachingConnectors()
105                         .stream()
106                         .filter((x) -> x.transport().name().equals("dt_socket"))
107                         .findFirst()
108                         .orElseThrow(() -> new Error("Could not find dt_socket connector!"));
109         Map<String, Connector.Argument> params = conn.defaultArguments();
110         params.get("port").setValue(port);
111         params.get("hostname").setValue("localhost");
112         // Timeout after 1 minute
113         params.get("timeout").setValue("60000");
114         return conn.attach(params);
115     }
116 
forwardJdwp(String pid)117     private String forwardJdwp(String pid) throws Exception {
118         // Try to have adb figure out the port number.
119         String result = mDevice.executeAdbCommand("forward", "tcp:0", "jdwp:" + pid);
120         if (result != null) {
121             return result.trim();
122         }
123         // We might be using an ancient adb. Try using a static port number instead. Number chosen
124         // arbitrarially. '15002' does not appear in any file as anything resembling a port number
125         // as far as I can tell.
126         final String port = "15002";
127         result = mDevice.executeAdbCommand("forward", "tcp:" + port, "jdwp:" + pid);
128         assertTrue(result != null);
129         return port;
130     }
131 
startupForwarding(String packageName, String shortClassName, boolean debug)132     private String startupForwarding(String packageName, String shortClassName, boolean debug)
133             throws Exception {
134         return startupForwarding(packageName, shortClassName, debug, false);
135     }
136 
startupForwarding(String packageName, String shortClassName, boolean debug, boolean startSuspended)137     private String startupForwarding(String packageName, String shortClassName, boolean debug,
138             boolean startSuspended) throws Exception {
139         moveToHomeScreen();
140         new Thread(() -> {
141             try {
142                 String cmd = "cmd activity start-activity " + (debug ? "-D" : "")
143                         + (startSuspended ? " --suspend" : "") + " -W -n " + packageName + "/."
144                         + shortClassName;
145                 CLog.i(cmd);
146                 mDevice.executeShellCommand(cmd);
147             } catch (DeviceNotAvailableException e) {
148                 CLog.i("Failed to start activity for package: " + packageName, e);
149             }
150         }).start();
151 
152         // Don't keep trying after a minute.
153         final Instant deadline = Instant.now().plusSeconds(60);
154         String pid = "";
155         while ((pid = mDevice.executeShellCommand("pidof " + packageName).trim()).equals("")) {
156             if (Instant.now().isAfter(deadline)) {
157                 fail("Unable to find PID of " + packageName + " process!");
158             }
159             // Wait 1 second and try again.
160             RunUtil.getDefault().sleep(1000);
161         }
162         String port = forwardJdwp(pid);
163         assertTrue(!"".equals(port));
164         return port;
165     }
166 
startupTest(String packageName, String shortClassName)167     private VirtualMachine startupTest(String packageName, String shortClassName) throws Exception {
168         return getDebuggerConnection(startupForwarding(packageName, shortClassName, true, false));
169     }
170 
startupTest(String packageName, String shortClassName, boolean debug, boolean startSuspended)171     private VirtualMachine startupTest(String packageName, String shortClassName, boolean debug,
172             boolean startSuspended) throws Exception {
173         return getDebuggerConnection(
174                 startupForwarding(packageName, shortClassName, debug, startSuspended));
175     }
176 
177     /**
178      * Tests that we can attach a debugger and perform basic debugging functions.
179      *
180      * We start the app with Wait-for-debugger. Wait for the ClassPrepare of the activity class and
181      * put and wait for a breakpoint on the onCreate function.
182      *
183      * TODO: We should expand this to more functions.
184      */
testAttachDebugger(String packageName, String shortClassName)185     private void testAttachDebugger(String packageName, String shortClassName)
186             throws DeviceNotAvailableException, Exception {
187         String fullClassName = packageName + "." + shortClassName;
188 
189         VirtualMachine vm = startupTest(packageName, shortClassName);
190         EventRequestManager erm = vm.eventRequestManager();
191         try {
192             // Just pause the runtime so it won't get ahead of us while we setup everything.
193             vm.suspend();
194             // Overall timeout for this whole test. 2-minutes
195             final Instant deadline = Instant.now().plusSeconds(120);
196             // Check the test-activity class is not already loaded.
197             assertTrue(shortClassName + " is not yet loaded!",
198                     vm.allClasses().stream().noneMatch(x -> x.name().equals(fullClassName)));
199 
200             // Wait for the class to load.
201             ClassPrepareRequest cpr = erm.createClassPrepareRequest();
202             cpr.addClassFilter(fullClassName);
203             cpr.setSuspendPolicy(EventRequest.SUSPEND_ALL);
204             cpr.enable();
205             vm.resume();
206             ReferenceType activityType = null;
207             while (activityType == null) {
208                 if (Instant.now().isAfter(deadline)) {
209                     fail(fullClassName + " did not load within timeout!");
210                 }
211                 activityType = vm.eventQueue()
212                                        .remove()
213                                        .stream()
214                                        .filter(e -> cpr == e.request())
215                                        .findFirst()
216                                        .map(e -> ((ClassPrepareEvent) e).referenceType())
217                                        .orElse(null);
218             }
219             cpr.disable();
220             // Set a breakpoint on the onCreate method at the first line.
221             BreakpointRequest bpr = erm.createBreakpointRequest(
222                     activityType.methodsByName("onCreate").get(0).allLineLocations().get(0));
223             bpr.setSuspendPolicy(EventRequest.SUSPEND_ALL);
224             bpr.enable();
225             vm.resume();
226 
227             // Wait for the event.
228             while (!vm.eventQueue().remove().stream().anyMatch(e -> e.request() == bpr)) {
229                 if (Instant.now().isAfter(deadline)) {
230                     fail(fullClassName + " did hit onCreate breakpoint within timeout!");
231                 }
232             }
233             bpr.disable();
234             vm.resume();
235         } finally {
236             // Always cleanup.
237             vm.dispose();
238         }
239     }
240 
241     /**
242      * Tests that we can attach a debugger and perform basic debugging functions to a
243      * debuggable app.
244      *
245      * We start the app with Wait-for-debugger. Wait for the ClassPrepare of the activity
246      * class and put and wait for a breakpoint on the onCreate function.
247      */
248     @Test
testAttachDebuggerToDebuggableApp()249     public void testAttachDebuggerToDebuggableApp() throws DeviceNotAvailableException, Exception {
250         testAttachDebugger(
251                 DEBUGGABLE_TEST_APP_PACKAGE_NAME, DEBUGGABLE_TEST_APP_ACTIVITY_CLASS_NAME);
252     }
253 
254     /**
255      * Tests that we CANNOT attach a debugger to a non-debuggable-but-profileable app.
256      *
257      * We test the attempt to attach the debuggable should fail on a user build device at the
258      * expected API call.
259      */
260     @Test
testAttachDebuggerToProfileableApp()261     public void testAttachDebuggerToProfileableApp() throws DeviceNotAvailableException, Exception {
262         java.io.IOException thrownException = null;
263         try {
264             testAttachDebugger(
265                     PROFILEABLE_TEST_APP_PACKAGE_NAME, PROFILEABLE_TEST_APP_ACTIVITY_CLASS_NAME);
266         } catch (java.io.IOException e) {
267             thrownException = e;
268         }
269         // Jdwp is only enabled on eng builds or when persist.debug.dalvik.vm.jdwp.enabled is set.
270         // Check that we are able to attach a debugger in these cases.
271         String buildType = mDevice.getProperty("ro.build.type");
272         String enableJdwp = mDevice.getProperty("persist.debug.dalvik.vm.jdwp.enabled");
273         if (buildType.equals("eng") || (enableJdwp != null && enableJdwp.equals("1"))) {
274             assertNull(thrownException);
275             return;
276         }
277         // We are on a device that doesn't enable jdwp.
278         assertNotNull(thrownException);
279         if (thrownException != null) {
280             // Verify the exception is thrown from the "getDebuggerConnection" method in this test
281             // when it calls the "attach" method from class AttachingConnector or its subclass.
282             // In other words, the callstack is expected to look like
283             //
284             // at
285             // jdk.jdi/com.sun.tools.jdi.SocketAttachingConnector.attach
286             // (SocketAttachingConnector.java:83)
287             // at
288             // android.jdwptunnel.cts.JdwpTunnelTest.getDebuggerConnection
289             // (JdwpTunnelTest.java:96)
290             boolean thrownByGetDebuggerConnection = false;
291             StackTraceElement[] stack = thrownException.getStackTrace();
292             for (int i = 0; i < stack.length; i++) {
293                 if (stack[i].getClassName().equals("android.jdwptunnel.cts.JdwpTunnelTest")
294                         && stack[i].getMethodName().equals("getDebuggerConnection")) {
295                     thrownByGetDebuggerConnection = true;
296                     assertTrue(i > 0);
297                     assertEquals("attach", stack[i - 1].getMethodName());
298                     break;
299                 }
300             }
301             assertTrue(thrownByGetDebuggerConnection);
302         }
303     }
304 
getDeviceBaseArch()305     private String getDeviceBaseArch() throws Exception {
306         String abi = mDevice.executeShellCommand("getprop ro.product.cpu.abi").replace("\n", "");
307         return AbiUtils.getBaseArchForAbi(abi);
308     }
309 
310     /**
311      * Tests that we don't get any DDMS messages before the handshake.
312      *
313      * <p>Since DDMS can send asynchronous replies it could race with the JDWP handshake. This could
314      * confuse clients. See bug: 178655046
315      */
316     @Test
testDdmsWaitsForHandshake()317     public void testDdmsWaitsForHandshake() throws DeviceNotAvailableException, Exception {
318         // Skip this test if not running on the device's native abi.
319         String testingArch = AbiUtils.getBaseArchForAbi(getAbi().getName());
320         String deviceArch = getDeviceBaseArch();
321         Assume.assumeTrue(testingArch.equals(deviceArch));
322 
323         String port = startupForwarding(
324                 DDMS_TEST_APP_PACKAGE_NAME, DDMS_TEST_APP_ACTIVITY_CLASS_NAME, false);
325         Socket sock = new Socket("localhost", Integer.decode(port).intValue());
326         OutputStream os = sock.getOutputStream();
327         // Let the test spin a bit. Try to lose any race with the app.
328         RunUtil.getDefault().sleep(1000);
329         String handshake = "JDWP-Handshake";
330         byte[] handshake_bytes = handshake.getBytes("US-ASCII");
331         os.write(handshake_bytes);
332         os.flush();
333         InputStream is = sock.getInputStream();
334         // Make sure we get the handshake first.
335         for (byte b : handshake_bytes) {
336             assertEquals(b, is.read());
337         }
338 
339         // Don't require anything in particular next since lots of things can send
340         // DDMS packets. Since there is no debugger connection we can assert that
341         // it is a DDMS packet at least by looking for a negative id.
342         // Skip the length
343         is.skip(4);
344         // Data sent big-endian so first byte has sign bit.
345         assertTrue((is.read() & 0x80) == 0x80);
346     }
347 
testThreadSuspensionState(VirtualMachine vm, boolean expected)348     private boolean testThreadSuspensionState(VirtualMachine vm, boolean expected) {
349         for (ThreadReference tr : vm.allThreads()) {
350             boolean isSuspended = tr.isSuspended();
351             if (isSuspended != expected) {
352                 return false;
353             }
354         }
355         return true;
356     }
357 
dumpThreads(VirtualMachine vm)358     private String dumpThreads(VirtualMachine vm) {
359         StringBuilder result = new StringBuilder();
360         for (ThreadReference tr : vm.allThreads()) {
361             result.append("Thread: '");
362             result.append(tr.name());
363             result.append("' isSuspended=");
364             result.append(tr.isSuspended());
365             result.append("\n");
366         }
367         return result.toString();
368     }
369 
assertThreadSuspensionState(VirtualMachine vm, boolean expected)370     private void assertThreadSuspensionState(VirtualMachine vm, boolean expected)
371             throws InterruptedException {
372         // If the debugger connects too fast, the VM may not have had time to hit the
373         // suspension point. We try several times to remedy to this problem.
374         for (int i = 0; i < 4; i++) {
375             if (testThreadSuspensionState(vm, expected)) {
376                 return;
377             }
378             Thread.sleep(1000);
379         }
380         fail("Threads are in unexpected state (expected=" + expected + ")\n" + dumpThreads(vm));
381     }
382 
383     // App can be started "suspended" which means all its threads will be suspended shorty after
384     // zygote specializes.
385     @Test
testSuspendStartup()386     public void testSuspendStartup() throws DeviceNotAvailableException, Exception {
387 
388         VirtualMachine vm = startupTest(DEBUGGABLE_TEST_APP_PACKAGE_NAME,
389                 DEBUGGABLE_TEST_APP_ACTIVITY_CLASS_NAME, true, true);
390 
391         try {
392             // The VM was started in suspended mode.
393             assertThreadSuspensionState(vm, true);
394 
395             // Let's go!
396             vm.resume();
397             assertThreadSuspensionState(vm, false);
398         } finally {
399             vm.dispose();
400         }
401     }
402 }
403