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