• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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