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