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