1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php 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 com.android.ide.eclipse.ndk.internal.launch; 18 19 import com.android.annotations.NonNull; 20 import com.android.annotations.Nullable; 21 import com.android.ddmlib.AdbCommandRejectedException; 22 import com.android.ddmlib.AndroidDebugBridge; 23 import com.android.ddmlib.Client; 24 import com.android.ddmlib.CollectingOutputReceiver; 25 import com.android.ddmlib.IDevice; 26 import com.android.ddmlib.IDevice.DeviceUnixSocketNamespace; 27 import com.android.ddmlib.InstallException; 28 import com.android.ddmlib.ShellCommandUnresponsiveException; 29 import com.android.ddmlib.SyncException; 30 import com.android.ddmlib.TimeoutException; 31 import com.android.ide.common.xml.ManifestData; 32 import com.android.ide.common.xml.ManifestData.Activity; 33 import com.android.ide.eclipse.adt.AdtPlugin; 34 import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; 35 import com.android.ide.eclipse.adt.internal.launch.AndroidLaunchController; 36 import com.android.ide.eclipse.adt.internal.launch.DeviceChooserDialog; 37 import com.android.ide.eclipse.adt.internal.launch.DeviceChooserDialog.DeviceChooserResponse; 38 import com.android.ide.eclipse.adt.internal.launch.LaunchConfigDelegate; 39 import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; 40 import com.android.ide.eclipse.adt.internal.project.ProjectHelper; 41 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 42 import com.android.ide.eclipse.ndk.internal.NativeAbi; 43 import com.android.ide.eclipse.ndk.internal.NdkHelper; 44 import com.android.ide.eclipse.ndk.internal.NdkVariables; 45 import com.android.sdklib.AndroidVersion; 46 import com.android.sdklib.IAndroidTarget; 47 import com.google.common.base.Joiner; 48 49 import org.eclipse.cdt.core.model.ICProject; 50 import org.eclipse.cdt.debug.core.CDebugUtils; 51 import org.eclipse.cdt.debug.core.ICDTLaunchConfigurationConstants; 52 import org.eclipse.cdt.dsf.gdb.IGDBLaunchConfigurationConstants; 53 import org.eclipse.cdt.dsf.gdb.launching.GdbLaunchDelegate; 54 import org.eclipse.core.resources.IFile; 55 import org.eclipse.core.resources.IProject; 56 import org.eclipse.core.runtime.CoreException; 57 import org.eclipse.core.runtime.IPath; 58 import org.eclipse.core.runtime.IProgressMonitor; 59 import org.eclipse.core.runtime.Path; 60 import org.eclipse.core.variables.IStringVariableManager; 61 import org.eclipse.core.variables.IValueVariable; 62 import org.eclipse.core.variables.VariablesPlugin; 63 import org.eclipse.debug.core.DebugPlugin; 64 import org.eclipse.debug.core.ILaunch; 65 import org.eclipse.debug.core.ILaunchConfiguration; 66 import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; 67 import org.eclipse.jface.dialogs.Dialog; 68 69 import java.io.IOException; 70 import java.util.ArrayList; 71 import java.util.Collection; 72 import java.util.Collections; 73 import java.util.List; 74 import java.util.concurrent.CountDownLatch; 75 import java.util.concurrent.TimeUnit; 76 77 @SuppressWarnings("restriction") 78 public class NdkGdbLaunchDelegate extends GdbLaunchDelegate { 79 public static final String LAUNCH_TYPE_ID = 80 "com.android.ide.eclipse.ndk.debug.LaunchConfigType"; //$NON-NLS-1$ 81 82 private static final Joiner JOINER = Joiner.on(", ").skipNulls(); 83 84 private static final String DEBUG_SOCKET = "debugsock"; //$NON-NLS-1$ 85 86 @Override launch(ILaunchConfiguration config, String mode, ILaunch launch, IProgressMonitor monitor)87 public void launch(ILaunchConfiguration config, String mode, ILaunch launch, 88 IProgressMonitor monitor) throws CoreException { 89 boolean launched = doLaunch(config, mode, launch, monitor); 90 if (!launched) { 91 if (launch.canTerminate()) { 92 launch.terminate(); 93 } 94 DebugPlugin.getDefault().getLaunchManager().removeLaunch(launch); 95 } 96 } 97 doLaunch(final ILaunchConfiguration config, String mode, ILaunch launch, IProgressMonitor monitor)98 public boolean doLaunch(final ILaunchConfiguration config, String mode, ILaunch launch, 99 IProgressMonitor monitor) throws CoreException { 100 IProject project = null; 101 ICProject cProject = CDebugUtils.getCProject(config); 102 if (cProject != null) { 103 project = cProject.getProject(); 104 } 105 106 if (project == null) { 107 AdtPlugin.printErrorToConsole( 108 Messages.NdkGdbLaunchDelegate_LaunchError_CouldNotGetProject); 109 return false; 110 } 111 112 // make sure the project and its dependencies are built and PostCompilerBuilder runs. 113 // This is a synchronous call which returns when the build is done. 114 monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_PerformIncrementalBuild); 115 ProjectHelper.doFullIncrementalDebugBuild(project, monitor); 116 117 // check if the project has errors, and abort in this case. 118 if (ProjectHelper.hasError(project, true)) { 119 AdtPlugin.printErrorToConsole(project, 120 Messages.NdkGdbLaunchDelegate_LaunchError_ProjectHasErrors); 121 return false; 122 } 123 124 final ManifestData manifestData = AndroidManifestHelper.parseForData(project); 125 final ManifestInfo manifestInfo = ManifestInfo.get(project); 126 final AndroidVersion minSdkVersion = new AndroidVersion( 127 manifestInfo.getMinSdkVersion(), 128 manifestInfo.getMinSdkCodeName()); 129 130 // Get the activity name to launch 131 String activityName = getActivityToLaunch( 132 getActivityNameInLaunchConfig(config), 133 manifestData.getLauncherActivity(), 134 manifestData.getActivities(), 135 project); 136 137 // Get ABI's supported by the application 138 monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_ObtainAppAbis); 139 Collection<NativeAbi> appAbis = NdkHelper.getApplicationAbis(project, monitor); 140 if (appAbis.size() == 0) { 141 AdtPlugin.printErrorToConsole(project, 142 Messages.NdkGdbLaunchDelegate_LaunchError_UnableToDetectAppAbi); 143 return false; 144 } 145 146 // Obtain device to use: 147 // - if there is only 1 device, just use that 148 // - if we have previously launched this config, and the device used is present, use that 149 // - otherwise show the DeviceChooserDialog 150 final String configName = config.getName(); 151 monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_ObtainDevice); 152 IDevice device = null; 153 IDevice[] devices = AndroidDebugBridge.getBridge().getDevices(); 154 if (devices.length == 1) { 155 device = devices[0]; 156 } else if ((device = getLastUsedDevice(config, devices)) == null) { 157 final IAndroidTarget projectTarget = Sdk.getCurrent().getTarget(project); 158 final DeviceChooserResponse response = new DeviceChooserResponse(); 159 final boolean continueLaunch[] = new boolean[] { false }; 160 AdtPlugin.getDisplay().syncExec(new Runnable() { 161 @Override 162 public void run() { 163 DeviceChooserDialog dialog = new DeviceChooserDialog( 164 AdtPlugin.getDisplay().getActiveShell(), 165 response, 166 manifestData.getPackage(), 167 projectTarget, minSdkVersion, false /*** FIXME! **/); 168 if (dialog.open() == Dialog.OK) { 169 AndroidLaunchController.updateLaunchConfigWithLastUsedDevice(config, 170 response); 171 continueLaunch[0] = true; 172 } 173 }; 174 }); 175 176 if (!continueLaunch[0]) { 177 return false; 178 } 179 180 device = response.getDeviceToUse(); 181 } 182 183 // ndk-gdb requires device > Froyo 184 monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_CheckAndroidDeviceVersion); 185 AndroidVersion deviceVersion = Sdk.getDeviceVersion(device); 186 if (deviceVersion == null) { 187 AdtPlugin.printErrorToConsole(project, 188 Messages.NdkGdbLaunchDelegate_LaunchError_UnknownAndroidDeviceVersion); 189 return false; 190 } else if (!deviceVersion.isGreaterOrEqualThan(8)) { 191 AdtPlugin.printErrorToConsole(project, 192 Messages.NdkGdbLaunchDelegate_LaunchError_Api8Needed); 193 return false; 194 } 195 196 // get Device ABI 197 monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_ObtainDeviceABI); 198 String deviceAbi1 = device.getProperty("ro.product.cpu.abi"); //$NON-NLS-1$ 199 String deviceAbi2 = device.getProperty("ro.product.cpu.abi2"); //$NON-NLS-1$ 200 201 // get the abi that is supported by both the device and the application 202 NativeAbi compatAbi = getCompatibleAbi(deviceAbi1, deviceAbi2, appAbis); 203 if (compatAbi == null) { 204 AdtPlugin.printErrorToConsole(project, 205 Messages.NdkGdbLaunchDelegate_LaunchError_NoCompatibleAbi); 206 AdtPlugin.printErrorToConsole(project, 207 String.format("ABI's supported by the application: %s", JOINER.join(appAbis))); 208 AdtPlugin.printErrorToConsole(project, 209 String.format("ABI's supported by the device: %s, %s", //$NON-NLS-1$ 210 deviceAbi1, 211 deviceAbi2)); 212 return false; 213 } 214 215 // sync app 216 monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_SyncAppToDevice); 217 IFile apk = ProjectHelper.getApplicationPackage(project); 218 if (apk == null) { 219 AdtPlugin.printErrorToConsole(project, 220 Messages.NdkGdbLaunchDelegate_LaunchError_NullApk); 221 return false; 222 } 223 try { 224 device.installPackage(apk.getLocation().toOSString(), true); 225 } catch (InstallException e1) { 226 AdtPlugin.printErrorToConsole(project, 227 Messages.NdkGdbLaunchDelegate_LaunchError_InstallError, e1); 228 return false; 229 } 230 231 // launch activity 232 monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_ActivityLaunch + activityName); 233 String command = String.format("am start -n %s/%s", manifestData.getPackage(), //$NON-NLS-1$ 234 activityName); 235 try { 236 CountDownLatch launchedLatch = new CountDownLatch(1); 237 CollectingOutputReceiver receiver = new CollectingOutputReceiver(launchedLatch); 238 device.executeShellCommand(command, receiver); 239 launchedLatch.await(5, TimeUnit.SECONDS); 240 String shellOutput = receiver.getOutput(); 241 if (shellOutput.contains("Error type")) { //$NON-NLS-1$ 242 throw new RuntimeException(receiver.getOutput()); 243 } 244 } catch (Exception e) { 245 AdtPlugin.printErrorToConsole(project, 246 Messages.NdkGdbLaunchDelegate_LaunchError_ActivityLaunchError, e); 247 return false; 248 } 249 250 // kill existing gdbserver 251 monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_KillExistingGdbServer); 252 for (Client c: device.getClients()) { 253 String description = c.getClientData().getClientDescription(); 254 if (description != null && description.contains("gdbserver")) { //$NON-NLS-1$ 255 c.kill(); 256 } 257 } 258 259 // pull app_process & libc from the device 260 IPath solibFolder = project.getLocation().append("obj/local").append(compatAbi.getAbi()); 261 try { 262 pull(device, "/system/bin/app_process", solibFolder); //$NON-NLS-1$ 263 pull(device, "/system/lib/libc.so", solibFolder); //$NON-NLS-1$ 264 } catch (Exception e) { 265 AdtPlugin.printErrorToConsole(project, 266 Messages.NdkGdbLaunchDelegate_LaunchError_PullFileError, e); 267 return false; 268 } 269 270 // wait for a couple of seconds for activity to be launched 271 monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_WaitingForActivity); 272 try { 273 Thread.sleep(2000); 274 } catch (InterruptedException e1) { 275 // uninterrupted 276 } 277 278 // get pid of activity 279 Client app = device.getClient(manifestData.getPackage()); 280 int pid = app.getClientData().getPid(); 281 282 // launch gdbserver 283 monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_LaunchingGdbServer); 284 CountDownLatch attachLatch = new CountDownLatch(1); 285 GdbServerTask gdbServer = new GdbServerTask(device, manifestData.getPackage(), 286 DEBUG_SOCKET, pid, attachLatch); 287 new Thread(gdbServer, 288 String.format("gdbserver for %s", manifestData.getPackage())).start(); //$NON-NLS-1$ 289 290 // wait for gdbserver to attach 291 monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_WaitGdbServerAttach); 292 boolean attached = false; 293 try { 294 attached = attachLatch.await(3, TimeUnit.SECONDS); 295 } catch (InterruptedException e) { 296 AdtPlugin.printErrorToConsole(project, 297 Messages.NdkGdbLaunchDelegate_LaunchError_InterruptedWaitingForGdbserver); 298 return false; 299 } 300 301 // if gdbserver failed to attach, we report any errors that may have occurred 302 if (!attached) { 303 if (gdbServer.getLaunchException() != null) { 304 AdtPlugin.printErrorToConsole(project, 305 Messages.NdkGdbLaunchDelegate_LaunchError_gdbserverLaunchException, 306 gdbServer.getLaunchException()); 307 } else { 308 AdtPlugin.printErrorToConsole(project, 309 Messages.NdkGdbLaunchDelegate_LaunchError_gdbserverOutput, 310 gdbServer.getShellOutput()); 311 } 312 AdtPlugin.printErrorToConsole(project, 313 Messages.NdkGdbLaunchDelegate_LaunchError_VerifyIfDebugBuild); 314 315 // shut down the gdbserver thread 316 gdbServer.setCancelled(); 317 return false; 318 } 319 320 // Obtain application working directory 321 String appDir = null; 322 try { 323 appDir = getAppDirectory(device, manifestData.getPackage(), 5, TimeUnit.SECONDS); 324 } catch (Exception e) { 325 AdtPlugin.printErrorToConsole(project, 326 Messages.NdkGdbLaunchDelegate_LaunchError_ObtainingAppFolder, e); 327 return false; 328 } 329 330 // setup port forwarding between local port & remote (device) unix domain socket 331 monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_SettingUpPortForward); 332 String localport = config.getAttribute(IGDBLaunchConfigurationConstants.ATTR_PORT, 333 NdkLaunchConstants.DEFAULT_GDB_PORT); 334 try { 335 device.createForward(Integer.parseInt(localport), 336 String.format("%s/%s", appDir, DEBUG_SOCKET), //$NON-NLS-1$ 337 DeviceUnixSocketNamespace.FILESYSTEM); 338 } catch (Exception e) { 339 AdtPlugin.printErrorToConsole(project, 340 Messages.NdkGdbLaunchDelegate_LaunchError_PortForwarding, e); 341 return false; 342 } 343 344 // update launch attributes based on device 345 ILaunchConfiguration config2 = performVariableSubstitutions(config, project, compatAbi, 346 monitor); 347 348 // launch gdb 349 monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_LaunchHostGdb); 350 super.launch(config2, mode, launch, monitor); 351 return true; 352 } 353 354 @Nullable getLastUsedDevice(ILaunchConfiguration config, @NonNull IDevice[] devices)355 private IDevice getLastUsedDevice(ILaunchConfiguration config, @NonNull IDevice[] devices) { 356 try { 357 boolean reuse = config.getAttribute(LaunchConfigDelegate.ATTR_REUSE_LAST_USED_DEVICE, 358 false); 359 if (!reuse) { 360 return null; 361 } 362 363 String serial = config.getAttribute(LaunchConfigDelegate.ATTR_LAST_USED_DEVICE, 364 (String)null); 365 return AndroidLaunchController.getDeviceIfOnline(serial, devices); 366 } catch (CoreException e) { 367 return null; 368 } 369 } 370 pull(IDevice device, String remote, IPath solibFolder)371 private void pull(IDevice device, String remote, IPath solibFolder) throws 372 SyncException, IOException, AdbCommandRejectedException, TimeoutException { 373 String remoteFileName = new Path(remote).toFile().getName(); 374 String targetFile = solibFolder.append(remoteFileName).toString(); 375 device.pullFile(remote, targetFile); 376 } 377 performVariableSubstitutions(ILaunchConfiguration config, IProject project, NativeAbi compatAbi, IProgressMonitor monitor)378 private ILaunchConfiguration performVariableSubstitutions(ILaunchConfiguration config, 379 IProject project, NativeAbi compatAbi, IProgressMonitor monitor) throws CoreException { 380 ILaunchConfigurationWorkingCopy wcopy = config.getWorkingCopy(); 381 382 String toolchainPrefix = NdkHelper.getToolchainPrefix(project, compatAbi, monitor); 383 String gdb = toolchainPrefix + "gdb"; //$NON-NLS-1$ 384 385 IStringVariableManager manager = VariablesPlugin.getDefault().getStringVariableManager(); 386 IValueVariable ndkGdb = manager.newValueVariable(NdkVariables.NDK_GDB, 387 NdkVariables.NDK_GDB, true, gdb); 388 IValueVariable ndkProject = manager.newValueVariable(NdkVariables.NDK_PROJECT, 389 NdkVariables.NDK_PROJECT, true, project.getLocation().toOSString()); 390 IValueVariable ndkCompatAbi = manager.newValueVariable(NdkVariables.NDK_COMPAT_ABI, 391 NdkVariables.NDK_COMPAT_ABI, true, compatAbi.getAbi()); 392 393 IValueVariable[] ndkVars = new IValueVariable[] { ndkGdb, ndkProject, ndkCompatAbi }; 394 manager.addVariables(ndkVars); 395 396 // fix path to gdb 397 String userGdbPath = wcopy.getAttribute(NdkLaunchConstants.ATTR_NDK_GDB, 398 NdkLaunchConstants.DEFAULT_GDB); 399 wcopy.setAttribute(IGDBLaunchConfigurationConstants.ATTR_DEBUG_NAME, 400 elaborateExpression(manager, userGdbPath)); 401 402 // setup program name 403 wcopy.setAttribute(ICDTLaunchConfigurationConstants.ATTR_PROGRAM_NAME, 404 elaborateExpression(manager, NdkLaunchConstants.DEFAULT_PROGRAM)); 405 406 // fix solib paths 407 List<String> solibPaths = wcopy.getAttribute( 408 NdkLaunchConstants.ATTR_NDK_SOLIB, 409 Collections.singletonList(NdkLaunchConstants.DEFAULT_SOLIB_PATH)); 410 List<String> fixedSolibPaths = new ArrayList<String>(solibPaths.size()); 411 for (String u : solibPaths) { 412 fixedSolibPaths.add(elaborateExpression(manager, u)); 413 } 414 wcopy.setAttribute(IGDBLaunchConfigurationConstants.ATTR_DEBUGGER_SOLIB_PATH, 415 fixedSolibPaths); 416 417 manager.removeVariables(ndkVars); 418 419 return wcopy.doSave(); 420 } 421 elaborateExpression(IStringVariableManager manager, String expr)422 private String elaborateExpression(IStringVariableManager manager, String expr) 423 throws CoreException{ 424 boolean DEBUG = true; 425 426 String eval = manager.performStringSubstitution(expr); 427 if (DEBUG) { 428 AdtPlugin.printToConsole("Substitute: ", expr, " --> ", eval); 429 } 430 431 return eval; 432 } 433 434 /** 435 * Returns the activity name to launch. If the user has requested a particular activity to 436 * be launched, then this method will confirm that the requested activity is defined in the 437 * manifest. If the user has not specified any activities, then it returns the default 438 * launcher activity. 439 * @param activityNameInLaunchConfig activity to launch as requested by the user. 440 * @param activities list of activities as defined in the application's manifest 441 * @param project android project 442 * @return activity name that should be launched, or null if no launchable activity. 443 */ getActivityToLaunch(String activityNameInLaunchConfig, Activity launcherActivity, Activity[] activities, IProject project)444 private String getActivityToLaunch(String activityNameInLaunchConfig, Activity launcherActivity, 445 Activity[] activities, IProject project) { 446 if (activities.length == 0) { 447 AdtPlugin.printErrorToConsole(project, 448 Messages.NdkGdbLaunchDelegate_LaunchError_NoActivityInManifest); 449 return null; 450 } else if (activityNameInLaunchConfig == null && launcherActivity != null) { 451 return launcherActivity.getName(); 452 } else { 453 for (Activity a : activities) { 454 if (a != null && a.getName().equals(activityNameInLaunchConfig)) { 455 return activityNameInLaunchConfig; 456 } 457 } 458 459 AdtPlugin.printErrorToConsole(project, 460 Messages.NdkGdbLaunchDelegate_LaunchError_NoSuchActivity); 461 if (launcherActivity != null) { 462 return launcherActivity.getName(); 463 } else { 464 AdtPlugin.printErrorToConsole( 465 Messages.NdkGdbLaunchDelegate_LaunchError_NoLauncherActivity); 466 return null; 467 } 468 } 469 } 470 getCompatibleAbi(String deviceAbi1, String deviceAbi2, Collection<NativeAbi> appAbis)471 private NativeAbi getCompatibleAbi(String deviceAbi1, String deviceAbi2, 472 Collection<NativeAbi> appAbis) { 473 for (NativeAbi abi: appAbis) { 474 if (abi.getAbi().equals(deviceAbi1) || abi.getAbi().equals(deviceAbi2)) { 475 return abi; 476 } 477 } 478 479 return null; 480 } 481 482 /** Returns the name of the activity as defined in the launch configuration. */ getActivityNameInLaunchConfig(ILaunchConfiguration configuration)483 private String getActivityNameInLaunchConfig(ILaunchConfiguration configuration) { 484 String empty = ""; //$NON-NLS-1$ 485 String activityName; 486 try { 487 activityName = configuration.getAttribute(LaunchConfigDelegate.ATTR_ACTIVITY, empty); 488 } catch (CoreException e) { 489 return null; 490 } 491 492 return (activityName != empty) ? activityName : null; 493 } 494 getAppDirectory(IDevice device, String app, long timeout, TimeUnit timeoutUnit)495 private String getAppDirectory(IDevice device, String app, long timeout, TimeUnit timeoutUnit) 496 throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, 497 IOException, InterruptedException { 498 String command = String.format("run-as %s /system/bin/sh -c pwd", app); //$NON-NLS-1$ 499 500 CountDownLatch commandCompleteLatch = new CountDownLatch(1); 501 CollectingOutputReceiver receiver = new CollectingOutputReceiver(commandCompleteLatch); 502 device.executeShellCommand(command, receiver); 503 commandCompleteLatch.await(timeout, timeoutUnit); 504 return receiver.getOutput().trim(); 505 } 506 } 507