diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.ndk/src/com/android/ide/eclipse/ndk/internal/launch/NdkGdbLaunchDelegate.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.ndk/src/com/android/ide/eclipse/ndk/internal/launch/NdkGdbLaunchDelegate.java | 506 |
1 files changed, 506 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.ndk/src/com/android/ide/eclipse/ndk/internal/launch/NdkGdbLaunchDelegate.java b/eclipse/plugins/com.android.ide.eclipse.ndk/src/com/android/ide/eclipse/ndk/internal/launch/NdkGdbLaunchDelegate.java new file mode 100644 index 000000000..0b124f249 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.ndk/src/com/android/ide/eclipse/ndk/internal/launch/NdkGdbLaunchDelegate.java @@ -0,0 +1,506 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.ndk.internal.launch; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ddmlib.AdbCommandRejectedException; +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.Client; +import com.android.ddmlib.CollectingOutputReceiver; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.IDevice.DeviceUnixSocketNamespace; +import com.android.ddmlib.InstallException; +import com.android.ddmlib.ShellCommandUnresponsiveException; +import com.android.ddmlib.SyncException; +import com.android.ddmlib.TimeoutException; +import com.android.ide.common.xml.ManifestData; +import com.android.ide.common.xml.ManifestData.Activity; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; +import com.android.ide.eclipse.adt.internal.launch.AndroidLaunchController; +import com.android.ide.eclipse.adt.internal.launch.DeviceChooserDialog; +import com.android.ide.eclipse.adt.internal.launch.DeviceChooserDialog.DeviceChooserResponse; +import com.android.ide.eclipse.adt.internal.launch.LaunchConfigDelegate; +import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; +import com.android.ide.eclipse.adt.internal.project.ProjectHelper; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.ndk.internal.NativeAbi; +import com.android.ide.eclipse.ndk.internal.NdkHelper; +import com.android.ide.eclipse.ndk.internal.NdkVariables; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.google.common.base.Joiner; + +import org.eclipse.cdt.core.model.ICProject; +import org.eclipse.cdt.debug.core.CDebugUtils; +import org.eclipse.cdt.debug.core.ICDTLaunchConfigurationConstants; +import org.eclipse.cdt.dsf.gdb.IGDBLaunchConfigurationConstants; +import org.eclipse.cdt.dsf.gdb.launching.GdbLaunchDelegate; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.variables.IStringVariableManager; +import org.eclipse.core.variables.IValueVariable; +import org.eclipse.core.variables.VariablesPlugin; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.jface.dialogs.Dialog; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@SuppressWarnings("restriction") +public class NdkGdbLaunchDelegate extends GdbLaunchDelegate { + public static final String LAUNCH_TYPE_ID = + "com.android.ide.eclipse.ndk.debug.LaunchConfigType"; //$NON-NLS-1$ + + private static final Joiner JOINER = Joiner.on(", ").skipNulls(); + + private static final String DEBUG_SOCKET = "debugsock"; //$NON-NLS-1$ + + @Override + public void launch(ILaunchConfiguration config, String mode, ILaunch launch, + IProgressMonitor monitor) throws CoreException { + boolean launched = doLaunch(config, mode, launch, monitor); + if (!launched) { + if (launch.canTerminate()) { + launch.terminate(); + } + DebugPlugin.getDefault().getLaunchManager().removeLaunch(launch); + } + } + + public boolean doLaunch(final ILaunchConfiguration config, String mode, ILaunch launch, + IProgressMonitor monitor) throws CoreException { + IProject project = null; + ICProject cProject = CDebugUtils.getCProject(config); + if (cProject != null) { + project = cProject.getProject(); + } + + if (project == null) { + AdtPlugin.printErrorToConsole( + Messages.NdkGdbLaunchDelegate_LaunchError_CouldNotGetProject); + return false; + } + + // make sure the project and its dependencies are built and PostCompilerBuilder runs. + // This is a synchronous call which returns when the build is done. + monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_PerformIncrementalBuild); + ProjectHelper.doFullIncrementalDebugBuild(project, monitor); + + // check if the project has errors, and abort in this case. + if (ProjectHelper.hasError(project, true)) { + AdtPlugin.printErrorToConsole(project, + Messages.NdkGdbLaunchDelegate_LaunchError_ProjectHasErrors); + return false; + } + + final ManifestData manifestData = AndroidManifestHelper.parseForData(project); + final ManifestInfo manifestInfo = ManifestInfo.get(project); + final AndroidVersion minSdkVersion = new AndroidVersion( + manifestInfo.getMinSdkVersion(), + manifestInfo.getMinSdkCodeName()); + + // Get the activity name to launch + String activityName = getActivityToLaunch( + getActivityNameInLaunchConfig(config), + manifestData.getLauncherActivity(), + manifestData.getActivities(), + project); + + // Get ABI's supported by the application + monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_ObtainAppAbis); + Collection<NativeAbi> appAbis = NdkHelper.getApplicationAbis(project, monitor); + if (appAbis.size() == 0) { + AdtPlugin.printErrorToConsole(project, + Messages.NdkGdbLaunchDelegate_LaunchError_UnableToDetectAppAbi); + return false; + } + + // Obtain device to use: + // - if there is only 1 device, just use that + // - if we have previously launched this config, and the device used is present, use that + // - otherwise show the DeviceChooserDialog + final String configName = config.getName(); + monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_ObtainDevice); + IDevice device = null; + IDevice[] devices = AndroidDebugBridge.getBridge().getDevices(); + if (devices.length == 1) { + device = devices[0]; + } else if ((device = getLastUsedDevice(config, devices)) == null) { + final IAndroidTarget projectTarget = Sdk.getCurrent().getTarget(project); + final DeviceChooserResponse response = new DeviceChooserResponse(); + final boolean continueLaunch[] = new boolean[] { false }; + AdtPlugin.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + DeviceChooserDialog dialog = new DeviceChooserDialog( + AdtPlugin.getDisplay().getActiveShell(), + response, + manifestData.getPackage(), + projectTarget, minSdkVersion, false /*** FIXME! **/); + if (dialog.open() == Dialog.OK) { + AndroidLaunchController.updateLaunchConfigWithLastUsedDevice(config, + response); + continueLaunch[0] = true; + } + }; + }); + + if (!continueLaunch[0]) { + return false; + } + + device = response.getDeviceToUse(); + } + + // ndk-gdb requires device > Froyo + monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_CheckAndroidDeviceVersion); + AndroidVersion deviceVersion = Sdk.getDeviceVersion(device); + if (deviceVersion == null) { + AdtPlugin.printErrorToConsole(project, + Messages.NdkGdbLaunchDelegate_LaunchError_UnknownAndroidDeviceVersion); + return false; + } else if (!deviceVersion.isGreaterOrEqualThan(8)) { + AdtPlugin.printErrorToConsole(project, + Messages.NdkGdbLaunchDelegate_LaunchError_Api8Needed); + return false; + } + + // get Device ABI + monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_ObtainDeviceABI); + String deviceAbi1 = device.getProperty("ro.product.cpu.abi"); //$NON-NLS-1$ + String deviceAbi2 = device.getProperty("ro.product.cpu.abi2"); //$NON-NLS-1$ + + // get the abi that is supported by both the device and the application + NativeAbi compatAbi = getCompatibleAbi(deviceAbi1, deviceAbi2, appAbis); + if (compatAbi == null) { + AdtPlugin.printErrorToConsole(project, + Messages.NdkGdbLaunchDelegate_LaunchError_NoCompatibleAbi); + AdtPlugin.printErrorToConsole(project, + String.format("ABI's supported by the application: %s", JOINER.join(appAbis))); + AdtPlugin.printErrorToConsole(project, + String.format("ABI's supported by the device: %s, %s", //$NON-NLS-1$ + deviceAbi1, + deviceAbi2)); + return false; + } + + // sync app + monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_SyncAppToDevice); + IFile apk = ProjectHelper.getApplicationPackage(project); + if (apk == null) { + AdtPlugin.printErrorToConsole(project, + Messages.NdkGdbLaunchDelegate_LaunchError_NullApk); + return false; + } + try { + device.installPackage(apk.getLocation().toOSString(), true); + } catch (InstallException e1) { + AdtPlugin.printErrorToConsole(project, + Messages.NdkGdbLaunchDelegate_LaunchError_InstallError, e1); + return false; + } + + // launch activity + monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_ActivityLaunch + activityName); + String command = String.format("am start -n %s/%s", manifestData.getPackage(), //$NON-NLS-1$ + activityName); + try { + CountDownLatch launchedLatch = new CountDownLatch(1); + CollectingOutputReceiver receiver = new CollectingOutputReceiver(launchedLatch); + device.executeShellCommand(command, receiver); + launchedLatch.await(5, TimeUnit.SECONDS); + String shellOutput = receiver.getOutput(); + if (shellOutput.contains("Error type")) { //$NON-NLS-1$ + throw new RuntimeException(receiver.getOutput()); + } + } catch (Exception e) { + AdtPlugin.printErrorToConsole(project, + Messages.NdkGdbLaunchDelegate_LaunchError_ActivityLaunchError, e); + return false; + } + + // kill existing gdbserver + monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_KillExistingGdbServer); + for (Client c: device.getClients()) { + String description = c.getClientData().getClientDescription(); + if (description != null && description.contains("gdbserver")) { //$NON-NLS-1$ + c.kill(); + } + } + + // pull app_process & libc from the device + IPath solibFolder = project.getLocation().append("obj/local").append(compatAbi.getAbi()); + try { + pull(device, "/system/bin/app_process", solibFolder); //$NON-NLS-1$ + pull(device, "/system/lib/libc.so", solibFolder); //$NON-NLS-1$ + } catch (Exception e) { + AdtPlugin.printErrorToConsole(project, + Messages.NdkGdbLaunchDelegate_LaunchError_PullFileError, e); + return false; + } + + // wait for a couple of seconds for activity to be launched + monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_WaitingForActivity); + try { + Thread.sleep(2000); + } catch (InterruptedException e1) { + // uninterrupted + } + + // get pid of activity + Client app = device.getClient(manifestData.getPackage()); + int pid = app.getClientData().getPid(); + + // launch gdbserver + monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_LaunchingGdbServer); + CountDownLatch attachLatch = new CountDownLatch(1); + GdbServerTask gdbServer = new GdbServerTask(device, manifestData.getPackage(), + DEBUG_SOCKET, pid, attachLatch); + new Thread(gdbServer, + String.format("gdbserver for %s", manifestData.getPackage())).start(); //$NON-NLS-1$ + + // wait for gdbserver to attach + monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_WaitGdbServerAttach); + boolean attached = false; + try { + attached = attachLatch.await(3, TimeUnit.SECONDS); + } catch (InterruptedException e) { + AdtPlugin.printErrorToConsole(project, + Messages.NdkGdbLaunchDelegate_LaunchError_InterruptedWaitingForGdbserver); + return false; + } + + // if gdbserver failed to attach, we report any errors that may have occurred + if (!attached) { + if (gdbServer.getLaunchException() != null) { + AdtPlugin.printErrorToConsole(project, + Messages.NdkGdbLaunchDelegate_LaunchError_gdbserverLaunchException, + gdbServer.getLaunchException()); + } else { + AdtPlugin.printErrorToConsole(project, + Messages.NdkGdbLaunchDelegate_LaunchError_gdbserverOutput, + gdbServer.getShellOutput()); + } + AdtPlugin.printErrorToConsole(project, + Messages.NdkGdbLaunchDelegate_LaunchError_VerifyIfDebugBuild); + + // shut down the gdbserver thread + gdbServer.setCancelled(); + return false; + } + + // Obtain application working directory + String appDir = null; + try { + appDir = getAppDirectory(device, manifestData.getPackage(), 5, TimeUnit.SECONDS); + } catch (Exception e) { + AdtPlugin.printErrorToConsole(project, + Messages.NdkGdbLaunchDelegate_LaunchError_ObtainingAppFolder, e); + return false; + } + + // setup port forwarding between local port & remote (device) unix domain socket + monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_SettingUpPortForward); + String localport = config.getAttribute(IGDBLaunchConfigurationConstants.ATTR_PORT, + NdkLaunchConstants.DEFAULT_GDB_PORT); + try { + device.createForward(Integer.parseInt(localport), + String.format("%s/%s", appDir, DEBUG_SOCKET), //$NON-NLS-1$ + DeviceUnixSocketNamespace.FILESYSTEM); + } catch (Exception e) { + AdtPlugin.printErrorToConsole(project, + Messages.NdkGdbLaunchDelegate_LaunchError_PortForwarding, e); + return false; + } + + // update launch attributes based on device + ILaunchConfiguration config2 = performVariableSubstitutions(config, project, compatAbi, + monitor); + + // launch gdb + monitor.setTaskName(Messages.NdkGdbLaunchDelegate_Action_LaunchHostGdb); + super.launch(config2, mode, launch, monitor); + return true; + } + + @Nullable + private IDevice getLastUsedDevice(ILaunchConfiguration config, @NonNull IDevice[] devices) { + try { + boolean reuse = config.getAttribute(LaunchConfigDelegate.ATTR_REUSE_LAST_USED_DEVICE, + false); + if (!reuse) { + return null; + } + + String serial = config.getAttribute(LaunchConfigDelegate.ATTR_LAST_USED_DEVICE, + (String)null); + return AndroidLaunchController.getDeviceIfOnline(serial, devices); + } catch (CoreException e) { + return null; + } + } + + private void pull(IDevice device, String remote, IPath solibFolder) throws + SyncException, IOException, AdbCommandRejectedException, TimeoutException { + String remoteFileName = new Path(remote).toFile().getName(); + String targetFile = solibFolder.append(remoteFileName).toString(); + device.pullFile(remote, targetFile); + } + + private ILaunchConfiguration performVariableSubstitutions(ILaunchConfiguration config, + IProject project, NativeAbi compatAbi, IProgressMonitor monitor) throws CoreException { + ILaunchConfigurationWorkingCopy wcopy = config.getWorkingCopy(); + + String toolchainPrefix = NdkHelper.getToolchainPrefix(project, compatAbi, monitor); + String gdb = toolchainPrefix + "gdb"; //$NON-NLS-1$ + + IStringVariableManager manager = VariablesPlugin.getDefault().getStringVariableManager(); + IValueVariable ndkGdb = manager.newValueVariable(NdkVariables.NDK_GDB, + NdkVariables.NDK_GDB, true, gdb); + IValueVariable ndkProject = manager.newValueVariable(NdkVariables.NDK_PROJECT, + NdkVariables.NDK_PROJECT, true, project.getLocation().toOSString()); + IValueVariable ndkCompatAbi = manager.newValueVariable(NdkVariables.NDK_COMPAT_ABI, + NdkVariables.NDK_COMPAT_ABI, true, compatAbi.getAbi()); + + IValueVariable[] ndkVars = new IValueVariable[] { ndkGdb, ndkProject, ndkCompatAbi }; + manager.addVariables(ndkVars); + + // fix path to gdb + String userGdbPath = wcopy.getAttribute(NdkLaunchConstants.ATTR_NDK_GDB, + NdkLaunchConstants.DEFAULT_GDB); + wcopy.setAttribute(IGDBLaunchConfigurationConstants.ATTR_DEBUG_NAME, + elaborateExpression(manager, userGdbPath)); + + // setup program name + wcopy.setAttribute(ICDTLaunchConfigurationConstants.ATTR_PROGRAM_NAME, + elaborateExpression(manager, NdkLaunchConstants.DEFAULT_PROGRAM)); + + // fix solib paths + List<String> solibPaths = wcopy.getAttribute( + NdkLaunchConstants.ATTR_NDK_SOLIB, + Collections.singletonList(NdkLaunchConstants.DEFAULT_SOLIB_PATH)); + List<String> fixedSolibPaths = new ArrayList<String>(solibPaths.size()); + for (String u : solibPaths) { + fixedSolibPaths.add(elaborateExpression(manager, u)); + } + wcopy.setAttribute(IGDBLaunchConfigurationConstants.ATTR_DEBUGGER_SOLIB_PATH, + fixedSolibPaths); + + manager.removeVariables(ndkVars); + + return wcopy.doSave(); + } + + private String elaborateExpression(IStringVariableManager manager, String expr) + throws CoreException{ + boolean DEBUG = true; + + String eval = manager.performStringSubstitution(expr); + if (DEBUG) { + AdtPlugin.printToConsole("Substitute: ", expr, " --> ", eval); + } + + return eval; + } + + /** + * Returns the activity name to launch. If the user has requested a particular activity to + * be launched, then this method will confirm that the requested activity is defined in the + * manifest. If the user has not specified any activities, then it returns the default + * launcher activity. + * @param activityNameInLaunchConfig activity to launch as requested by the user. + * @param activities list of activities as defined in the application's manifest + * @param project android project + * @return activity name that should be launched, or null if no launchable activity. + */ + private String getActivityToLaunch(String activityNameInLaunchConfig, Activity launcherActivity, + Activity[] activities, IProject project) { + if (activities.length == 0) { + AdtPlugin.printErrorToConsole(project, + Messages.NdkGdbLaunchDelegate_LaunchError_NoActivityInManifest); + return null; + } else if (activityNameInLaunchConfig == null && launcherActivity != null) { + return launcherActivity.getName(); + } else { + for (Activity a : activities) { + if (a != null && a.getName().equals(activityNameInLaunchConfig)) { + return activityNameInLaunchConfig; + } + } + + AdtPlugin.printErrorToConsole(project, + Messages.NdkGdbLaunchDelegate_LaunchError_NoSuchActivity); + if (launcherActivity != null) { + return launcherActivity.getName(); + } else { + AdtPlugin.printErrorToConsole( + Messages.NdkGdbLaunchDelegate_LaunchError_NoLauncherActivity); + return null; + } + } + } + + private NativeAbi getCompatibleAbi(String deviceAbi1, String deviceAbi2, + Collection<NativeAbi> appAbis) { + for (NativeAbi abi: appAbis) { + if (abi.getAbi().equals(deviceAbi1) || abi.getAbi().equals(deviceAbi2)) { + return abi; + } + } + + return null; + } + + /** Returns the name of the activity as defined in the launch configuration. */ + private String getActivityNameInLaunchConfig(ILaunchConfiguration configuration) { + String empty = ""; //$NON-NLS-1$ + String activityName; + try { + activityName = configuration.getAttribute(LaunchConfigDelegate.ATTR_ACTIVITY, empty); + } catch (CoreException e) { + return null; + } + + return (activityName != empty) ? activityName : null; + } + + private String getAppDirectory(IDevice device, String app, long timeout, TimeUnit timeoutUnit) + throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, + IOException, InterruptedException { + String command = String.format("run-as %s /system/bin/sh -c pwd", app); //$NON-NLS-1$ + + CountDownLatch commandCompleteLatch = new CountDownLatch(1); + CollectingOutputReceiver receiver = new CollectingOutputReceiver(commandCompleteLatch); + device.executeShellCommand(command, receiver); + commandCompleteLatch.await(timeout, timeoutUnit); + return receiver.getOutput().trim(); + } +} |