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 = 15;
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         Socket socket = new Socket();
228         DataInputStream traceDataStream = null;
229         DataOutputStream traceCommandsStream = null;
230         try {
231             socket.connect(new java.net.InetSocketAddress("127.0.0.1", port)); //$NON-NLS-1$
232             socket.setTcpNoDelay(true);
233             traceDataStream = new DataInputStream(socket.getInputStream());
234             traceCommandsStream = new DataOutputStream(socket.getOutputStream());
235         } catch (IOException e) {
236             MessageDialog.openError(shell,
237                     "OpenGL Trace",
238                     "Unable to connect to remote GL Trace Server: " + e.getMessage());
239             return;
240         }
241 
242         // create channel to send trace commands to device
243         TraceCommandWriter traceCommandWriter = new TraceCommandWriter(traceCommandsStream);
244         try {
245             traceCommandWriter.setTraceOptions(traceOptions.collectFbOnEglSwap,
246                     traceOptions.collectFbOnGlDraw,
247                     traceOptions.collectTextureData);
248         } catch (IOException e) {
249             MessageDialog.openError(shell,
250                     "OpenGL Trace",
251                     "Unexpected error while setting trace options: " + e.getMessage());
252             closeSocket(socket);
253             return;
254         }
255 
256         FileOutputStream fos = null;
257         try {
258             fos = new FileOutputStream(traceOptions.traceDestination, false);
259         } catch (FileNotFoundException e) {
260             // input path is valid, so this cannot occur
261         }
262 
263         // create trace writer that writes to a trace file
264         TraceFileWriter traceFileWriter = new TraceFileWriter(fos, traceDataStream);
265         traceFileWriter.start();
266 
267         GLTraceCollectorDialog dlg = new GLTraceCollectorDialog(shell,
268                 traceFileWriter,
269                 traceCommandWriter,
270                 traceOptions);
271         dlg.open();
272 
273         traceFileWriter.stopTracing();
274         traceCommandWriter.close();
275         closeSocket(socket);
276     }
277 
closeSocket(Socket socket)278     private static void closeSocket(Socket socket) {
279         try {
280             socket.close();
281         } catch (IOException e) {
282             // ignore error while closing socket
283         }
284     }
285 
startActivity(IDevice device, String appPackage, String activity, boolean isActivityNameFullyQualified)286     private void startActivity(IDevice device, String appPackage, String activity,
287             boolean isActivityNameFullyQualified)
288             throws TimeoutException, AdbCommandRejectedException,
289             ShellCommandUnresponsiveException, IOException, InterruptedException {
290         killApp(device, appPackage); // kill app if it is already running
291         waitUntilAppKilled(device, appPackage, KILL_TIMEOUT);
292 
293         StringBuilder activityPath = new StringBuilder(appPackage);
294         if (!activity.isEmpty()) {
295             activityPath.append('/');
296             if (!isActivityNameFullyQualified) {
297                 activityPath.append('.');
298             }
299             activityPath.append(activity);
300         }
301         String startAppCmd = String.format(
302                 "am start --opengl-trace %s -a android.intent.action.MAIN -c android.intent.category.LAUNCHER", //$NON-NLS-1$
303                 activityPath.toString());
304 
305         Semaphore launchCompletionSempahore = new Semaphore(0);
306         StartActivityOutputReceiver receiver = new StartActivityOutputReceiver(
307                 launchCompletionSempahore);
308         device.executeShellCommand(startAppCmd, receiver);
309 
310         // wait until shell finishes launch command
311         launchCompletionSempahore.acquire();
312 
313         // throw exception if there was an error during launch
314         String output = receiver.getOutput();
315         if (output.contains("Error")) {             //$NON-NLS-1$
316             throw new RuntimeException(output);
317         }
318 
319         // wait until the app itself has been launched
320         waitUntilAppLaunched(device, appPackage, LAUNCH_TIMEOUT);
321     }
322 
killApp(IDevice device, String appName)323     private void killApp(IDevice device, String appName) {
324         Client client = device.getClient(appName);
325         if (client != null) {
326             client.kill();
327         }
328     }
329 
waitUntilAppLaunched(final IDevice device, final String appName, int timeout)330     private void waitUntilAppLaunched(final IDevice device, final String appName, int timeout) {
331         Callable<Boolean> c = new Callable<Boolean>() {
332             @Override
333             public Boolean call() throws Exception {
334                 Client client;
335                 do {
336                     client = device.getClient(appName);
337                 } while (client == null);
338 
339                 return Boolean.TRUE;
340             }
341         };
342         try {
343             new SimpleTimeLimiter().callWithTimeout(c, timeout, TimeUnit.SECONDS, true);
344         } catch (Exception e) {
345             throw new RuntimeException("Timed out waiting for application to launch.");
346         }
347 
348         // once the app has launched, wait an additional couple of seconds
349         // for it to start up
350         try {
351             Thread.sleep(2000);
352         } catch (InterruptedException e) {
353             // ignore
354         }
355     }
356 
waitUntilAppKilled(final IDevice device, final String appName, int timeout)357     private void waitUntilAppKilled(final IDevice device, final String appName, int timeout) {
358         Callable<Boolean> c = new Callable<Boolean>() {
359             @Override
360             public Boolean call() throws Exception {
361                 Client client;
362                 while ((client = device.getClient(appName)) != null) {
363                     client.kill();
364                 }
365                 return Boolean.TRUE;
366             }
367         };
368         try {
369             new SimpleTimeLimiter().callWithTimeout(c, timeout, TimeUnit.SECONDS, true);
370         } catch (Exception e) {
371             throw new RuntimeException("Timed out waiting for running application to die.");
372         }
373     }
374 
setupForwarding(IDevice device, int i)375     public static void setupForwarding(IDevice device, int i)
376             throws TimeoutException, AdbCommandRejectedException, IOException {
377         device.createForward(i, GLTRACE_UDS, DeviceUnixSocketNamespace.ABSTRACT);
378     }
379 
disablePortForwarding(IDevice device, int port)380     public static void disablePortForwarding(IDevice device, int port) {
381         try {
382             device.removeForward(port, GLTRACE_UDS, DeviceUnixSocketNamespace.ABSTRACT);
383         } catch (Exception e) {
384             // ignore exceptions;
385         }
386     }
387 
getDevice(String deviceName)388     private IDevice getDevice(String deviceName) {
389         IDevice[] devices = AndroidDebugBridge.getBridge().getDevices();
390 
391         for (IDevice device : devices) {
392             if (device.getName().equals(deviceName)) {
393                 return device;
394             }
395         }
396 
397         return null;
398     }
399 
400     private static class StartActivityOutputReceiver implements IShellOutputReceiver {
401         private Semaphore mSemaphore;
402         private StringBuffer sb = new StringBuffer(300);
403 
StartActivityOutputReceiver(Semaphore s)404         public StartActivityOutputReceiver(Semaphore s) {
405             mSemaphore = s;
406         }
407 
408         @Override
addOutput(byte[] data, int offset, int length)409         public void addOutput(byte[] data, int offset, int length) {
410             String d = new String(data, offset, length);
411             sb.append(d);
412         }
413 
414         @Override
flush()415         public void flush() {
416             mSemaphore.release();
417         }
418 
419         @Override
isCancelled()420         public boolean isCancelled() {
421             return false;
422         }
423 
getOutput()424         public String getOutput() {
425             return sb.toString();
426         }
427     }
428 }
429