• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
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.gltrace;
18 
19 import com.android.ddmlib.AdbCommandRejectedException;
20 import com.android.ddmlib.AndroidDebugBridge;
21 import com.android.ddmlib.Client;
22 import com.android.ddmlib.IDevice;
23 import com.android.ddmlib.IDevice.DeviceUnixSocketNamespace;
24 import com.android.ddmlib.IShellOutputReceiver;
25 import com.android.ddmlib.ShellCommandUnresponsiveException;
26 import com.android.ddmlib.TimeoutException;
27 import com.android.ide.eclipse.gltrace.editors.GLFunctionTraceViewer;
28 import com.google.common.io.Closeables;
29 import com.google.common.util.concurrent.SimpleTimeLimiter;
30 
31 import org.eclipse.core.filesystem.EFS;
32 import org.eclipse.core.filesystem.IFileStore;
33 import org.eclipse.core.runtime.Path;
34 import org.eclipse.jface.action.IAction;
35 import org.eclipse.jface.dialogs.MessageDialog;
36 import org.eclipse.jface.viewers.ISelection;
37 import org.eclipse.jface.window.Window;
38 import org.eclipse.swt.widgets.Display;
39 import org.eclipse.swt.widgets.Shell;
40 import org.eclipse.ui.IEditorInput;
41 import org.eclipse.ui.IEditorReference;
42 import org.eclipse.ui.IURIEditorInput;
43 import org.eclipse.ui.IWorkbench;
44 import org.eclipse.ui.IWorkbenchPage;
45 import org.eclipse.ui.IWorkbenchWindow;
46 import org.eclipse.ui.IWorkbenchWindowActionDelegate;
47 import org.eclipse.ui.PartInitException;
48 import org.eclipse.ui.PlatformUI;
49 import org.eclipse.ui.WorkbenchException;
50 import org.eclipse.ui.ide.IDE;
51 
52 import java.io.DataInputStream;
53 import java.io.DataOutputStream;
54 import java.io.FileNotFoundException;
55 import java.io.FileOutputStream;
56 import java.io.IOException;
57 import java.net.Socket;
58 import java.util.concurrent.Callable;
59 import java.util.concurrent.Semaphore;
60 import java.util.concurrent.TimeUnit;
61 
62 public class CollectTraceAction implements IWorkbenchWindowActionDelegate {
63     /** Abstract Unix Domain Socket Name used by the gltrace device code. */
64     private static final String GLTRACE_UDS = "gltrace";        //$NON-NLS-1$
65 
66     /** Local port that is forwarded to the device's {@link #GLTRACE_UDS} socket. */
67     private static final int LOCAL_FORWARDED_PORT = 6039;
68 
69     /** Activity name to use for a system activity that has already been launched. */
70     private static final String SYSTEM_APP = "system";          //$NON-NLS-1$
71 
72     /** Time to wait for the application to launch (seconds) */
73     private static final int LAUNCH_TIMEOUT = 5;
74 
75     /** Time to wait for the application to die (seconds) */
76     private static final int KILL_TIMEOUT = 5;
77 
78     private static final int MIN_API_LEVEL = 16;
79 
80     @Override
run(IAction action)81     public void run(IAction action) {
82         connectToDevice();
83     }
84 
85     @Override
selectionChanged(IAction action, ISelection selection)86     public void selectionChanged(IAction action, ISelection selection) {
87     }
88 
89     @Override
dispose()90     public void dispose() {
91     }
92 
93     @Override
init(IWorkbenchWindow window)94     public void init(IWorkbenchWindow window) {
95     }
96 
connectToDevice()97     private void connectToDevice() {
98         Shell shell = Display.getDefault().getActiveShell();
99         GLTraceOptionsDialog dlg = new GLTraceOptionsDialog(shell);
100         if (dlg.open() != Window.OK) {
101             return;
102         }
103 
104         TraceOptions traceOptions = dlg.getTraceOptions();
105 
106         IDevice device = getDevice(traceOptions.device);
107         String apiLevelString = device.getProperty(IDevice.PROP_BUILD_API_LEVEL);
108         int apiLevel;
109         try {
110             apiLevel = Integer.parseInt(apiLevelString);
111         } catch (NumberFormatException e) {
112             apiLevel = MIN_API_LEVEL;
113         }
114         if (apiLevel < MIN_API_LEVEL) {
115             MessageDialog.openError(shell, "GL Trace",
116                     String.format("OpenGL Tracing is only supported on devices at API Level %1$d."
117                             + "The selected device '%2$s' provides API level %3$s.",
118                                     MIN_API_LEVEL, traceOptions.device, apiLevelString));
119             return;
120         }
121 
122         try {
123             setupForwarding(device, LOCAL_FORWARDED_PORT);
124         } catch (Exception e) {
125             MessageDialog.openError(shell, "Setup GL Trace",
126                     "Error while setting up port forwarding: " + e.getMessage());
127             return;
128         }
129 
130         try {
131             if (!SYSTEM_APP.equals(traceOptions.appToTrace)) {
132                 startActivity(device, traceOptions.appToTrace, traceOptions.activityToTrace,
133                         traceOptions.isActivityNameFullyQualified);
134             }
135         } catch (Exception e) {
136             MessageDialog.openError(shell, "Setup GL Trace",
137                     "Error while launching application: " + e.getMessage());
138             return;
139         }
140 
141         // if everything went well, the app should now be waiting for the gl debugger
142         // to connect
143         startTracing(shell, traceOptions, LOCAL_FORWARDED_PORT);
144 
145         // once tracing is complete, remove port forwarding
146         disablePortForwarding(device, LOCAL_FORWARDED_PORT);
147 
148         // and finally open the editor to view the file
149         openInEditor(shell, traceOptions.traceDestination);
150     }
151 
openInEditor(Shell shell, String traceFilePath)152     public static void openInEditor(Shell shell, String traceFilePath) {
153         final IFileStore fileStore = EFS.getLocalFileSystem().getStore(new Path(traceFilePath));
154         if (!fileStore.fetchInfo().exists()) {
155             return;
156         }
157 
158         final IWorkbench workbench = PlatformUI.getWorkbench();
159         IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
160         if (window == null) {
161             return;
162         }
163 
164         IWorkbenchPage page = window.getActivePage();
165         if (page == null) {
166             return;
167         }
168 
169         try {
170             workbench.showPerspective("com.android.ide.eclipse.gltrace.perspective", window);
171         } catch (WorkbenchException e) {
172         }
173 
174         // if there is a editor already open, then refresh its model
175         GLFunctionTraceViewer viewer = getOpenTraceViewer(page, traceFilePath);
176         if (viewer != null) {
177             viewer.setInput(shell, traceFilePath);
178         }
179 
180         // open the editor (if not open), or bring it to foreground if it is already open
181         try {
182             IDE.openEditorOnFileStore(page, fileStore);
183         } catch (PartInitException e) {
184             GlTracePlugin.getDefault().logMessage(
185                     "Unexpected error while opening gltrace file in editor: " + e);
186             return;
187         }
188     }
189 
190     /**
191      * Returns the editor part that has the provided file path open.
192      * @param page page containing editors
193      * @param traceFilePath file that should be open in an editor
194      * @return if given trace file is already open, then a reference to that editor part,
195      *         null otherwise
196      */
getOpenTraceViewer(IWorkbenchPage page, String traceFilePath)197     private static GLFunctionTraceViewer getOpenTraceViewer(IWorkbenchPage page,
198             String traceFilePath) {
199         IEditorReference[] editorRefs = page.getEditorReferences();
200         for (IEditorReference ref : editorRefs) {
201             String id = ref.getId();
202             if (!GLFunctionTraceViewer.ID.equals(id)) {
203                 continue;
204             }
205 
206             IEditorInput input = null;
207             try {
208                 input = ref.getEditorInput();
209             } catch (PartInitException e) {
210                 continue;
211             }
212 
213             if (!(input instanceof IURIEditorInput)) {
214                 continue;
215             }
216 
217             if (traceFilePath.equals(((IURIEditorInput) input).getURI().getPath())) {
218                 return (GLFunctionTraceViewer) ref.getEditor(true);
219             }
220         }
221 
222         return null;
223     }
224 
225     @SuppressWarnings("resource") // Closeables.closeQuietly
startTracing(Shell shell, TraceOptions traceOptions, int port)226     public static void startTracing(Shell shell, TraceOptions traceOptions, int port) {
227         FileOutputStream fos = null;
228         try {
229             fos = new FileOutputStream(traceOptions.traceDestination, false);
230         } catch (FileNotFoundException e) {
231             // input path is valid, so this cannot occur
232         }
233 
234         Socket socket = new Socket();
235         DataInputStream traceDataStream = null;
236         DataOutputStream traceCommandsStream = null;
237         try {
238             socket.connect(new java.net.InetSocketAddress("127.0.0.1", port)); //$NON-NLS-1$
239             socket.setTcpNoDelay(true);
240             traceDataStream = new DataInputStream(socket.getInputStream());
241             traceCommandsStream = new DataOutputStream(socket.getOutputStream());
242         } catch (IOException e) {
243             MessageDialog.openError(shell,
244                     "OpenGL Trace",
245                     "Unable to connect to remote GL Trace Server: " + e.getMessage());
246             Closeables.closeQuietly(fos);
247             return;
248         }
249 
250         // create channel to send trace commands to device
251         TraceCommandWriter traceCommandWriter = new TraceCommandWriter(traceCommandsStream);
252         try {
253             traceCommandWriter.setTraceOptions(traceOptions.collectFbOnEglSwap,
254                     traceOptions.collectFbOnGlDraw,
255                     traceOptions.collectTextureData);
256         } catch (IOException e) {
257             MessageDialog.openError(shell,
258                     "OpenGL Trace",
259                     "Unexpected error while setting trace options: " + e.getMessage());
260             closeSocket(socket);
261             Closeables.closeQuietly(fos);
262             return;
263         }
264 
265         // create trace writer that writes to a trace file
266         TraceFileWriter traceFileWriter = new TraceFileWriter(fos, traceDataStream);
267         traceFileWriter.start();
268 
269         GLTraceCollectorDialog dlg = new GLTraceCollectorDialog(shell,
270                 traceFileWriter,
271                 traceCommandWriter,
272                 traceOptions);
273         dlg.open();
274 
275         traceFileWriter.stopTracing();
276         traceCommandWriter.close();
277         closeSocket(socket);
278     }
279 
closeSocket(Socket socket)280     private static void closeSocket(Socket socket) {
281         try {
282             socket.close();
283         } catch (IOException e) {
284             // ignore error while closing socket
285         }
286     }
287 
startActivity(IDevice device, String appPackage, String activity, boolean isActivityNameFullyQualified)288     private void startActivity(IDevice device, String appPackage, String activity,
289             boolean isActivityNameFullyQualified)
290             throws TimeoutException, AdbCommandRejectedException,
291             ShellCommandUnresponsiveException, IOException, InterruptedException {
292         killApp(device, appPackage); // kill app if it is already running
293         waitUntilAppKilled(device, appPackage, KILL_TIMEOUT);
294 
295         StringBuilder activityPath = new StringBuilder(appPackage);
296         if (!activity.isEmpty()) {
297             activityPath.append('/');
298             if (!isActivityNameFullyQualified) {
299                 activityPath.append('.');
300             }
301             activityPath.append(activity);
302         }
303         String startAppCmd = String.format(
304                 "am start --opengl-trace %s -a android.intent.action.MAIN -c android.intent.category.LAUNCHER", //$NON-NLS-1$
305                 activityPath.toString());
306 
307         Semaphore launchCompletionSempahore = new Semaphore(0);
308         StartActivityOutputReceiver receiver = new StartActivityOutputReceiver(
309                 launchCompletionSempahore);
310         device.executeShellCommand(startAppCmd, receiver);
311 
312         // wait until shell finishes launch command
313         launchCompletionSempahore.acquire();
314 
315         // throw exception if there was an error during launch
316         String output = receiver.getOutput();
317         if (output.contains("Error")) {             //$NON-NLS-1$
318             throw new RuntimeException(output);
319         }
320 
321         // wait until the app itself has been launched
322         waitUntilAppLaunched(device, appPackage, LAUNCH_TIMEOUT);
323     }
324 
killApp(IDevice device, String appName)325     private void killApp(IDevice device, String appName) {
326         Client client = device.getClient(appName);
327         if (client != null) {
328             client.kill();
329         }
330     }
331 
waitUntilAppLaunched(final IDevice device, final String appName, int timeout)332     private void waitUntilAppLaunched(final IDevice device, final String appName, int timeout) {
333         Callable<Boolean> c = new Callable<Boolean>() {
334             @Override
335             public Boolean call() throws Exception {
336                 Client client;
337                 do {
338                     client = device.getClient(appName);
339                 } while (client == null);
340 
341                 return Boolean.TRUE;
342             }
343         };
344         try {
345             new SimpleTimeLimiter().callWithTimeout(c, timeout, TimeUnit.SECONDS, true);
346         } catch (Exception e) {
347             throw new RuntimeException("Timed out waiting for application to launch.");
348         }
349 
350         // once the app has launched, wait an additional couple of seconds
351         // for it to start up
352         try {
353             Thread.sleep(2000);
354         } catch (InterruptedException e) {
355             // ignore
356         }
357     }
358 
waitUntilAppKilled(final IDevice device, final String appName, int timeout)359     private void waitUntilAppKilled(final IDevice device, final String appName, int timeout) {
360         Callable<Boolean> c = new Callable<Boolean>() {
361             @Override
362             public Boolean call() throws Exception {
363                 Client client;
364                 while ((client = device.getClient(appName)) != null) {
365                     client.kill();
366                 }
367                 return Boolean.TRUE;
368             }
369         };
370         try {
371             new SimpleTimeLimiter().callWithTimeout(c, timeout, TimeUnit.SECONDS, true);
372         } catch (Exception e) {
373             throw new RuntimeException("Timed out waiting for running application to die.");
374         }
375     }
376 
setupForwarding(IDevice device, int i)377     public static void setupForwarding(IDevice device, int i)
378             throws TimeoutException, AdbCommandRejectedException, IOException {
379         device.createForward(i, GLTRACE_UDS, DeviceUnixSocketNamespace.ABSTRACT);
380     }
381 
disablePortForwarding(IDevice device, int port)382     public static void disablePortForwarding(IDevice device, int port) {
383         try {
384             device.removeForward(port, GLTRACE_UDS, DeviceUnixSocketNamespace.ABSTRACT);
385         } catch (Exception e) {
386             // ignore exceptions;
387         }
388     }
389 
getDevice(String deviceName)390     private IDevice getDevice(String deviceName) {
391         IDevice[] devices = AndroidDebugBridge.getBridge().getDevices();
392 
393         for (IDevice device : devices) {
394             if (device.getName().equals(deviceName)) {
395                 return device;
396             }
397         }
398 
399         return null;
400     }
401 
402     private static class StartActivityOutputReceiver implements IShellOutputReceiver {
403         private Semaphore mSemaphore;
404         private StringBuffer sb = new StringBuffer(300);
405 
StartActivityOutputReceiver(Semaphore s)406         public StartActivityOutputReceiver(Semaphore s) {
407             mSemaphore = s;
408         }
409 
410         @Override
addOutput(byte[] data, int offset, int length)411         public void addOutput(byte[] data, int offset, int length) {
412             String d = new String(data, offset, length);
413             sb.append(d);
414         }
415 
416         @Override
flush()417         public void flush() {
418             mSemaphore.release();
419         }
420 
421         @Override
isCancelled()422         public boolean isCancelled() {
423             return false;
424         }
425 
getOutput()426         public String getOutput() {
427             return sb.toString();
428         }
429     }
430 }
431