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 81 public void run(IAction action) { 82 connectToDevice(); 83 } 84 85 @Override 86 public void selectionChanged(IAction action, ISelection selection) { 87 } 88 89 @Override 90 public void dispose() { 91 } 92 93 @Override 94 public void init(IWorkbenchWindow window) { 95 } 96 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 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 */ 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 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 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 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 323 private void killApp(IDevice device, String appName) { 324 Client client = device.getClient(appName); 325 if (client != null) { 326 client.kill(); 327 } 328 } 329 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 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 375 public static void setupForwarding(IDevice device, int i) 376 throws TimeoutException, AdbCommandRejectedException, IOException { 377 device.createForward(i, GLTRACE_UDS, DeviceUnixSocketNamespace.ABSTRACT); 378 } 379 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 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 404 public StartActivityOutputReceiver(Semaphore s) { 405 mSemaphore = s; 406 } 407 408 @Override 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 415 public void flush() { 416 mSemaphore.release(); 417 } 418 419 @Override 420 public boolean isCancelled() { 421 return false; 422 } 423 424 public String getOutput() { 425 return sb.toString(); 426 } 427 } 428 } 429