| /* |
| * 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(); |
| } |
| } |