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