blob: c13ab461b44fc4fc6e8aa4a61f332b0bc9a3e1fe [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ide.eclipse.gltrace;
import com.android.ddmlib.AdbCommandRejectedException;
import com.android.ddmlib.AndroidDebugBridge;
import com.android.ddmlib.Client;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.IDevice.DeviceUnixSocketNamespace;
import com.android.ddmlib.IShellOutputReceiver;
import com.android.ddmlib.ShellCommandUnresponsiveException;
import com.android.ddmlib.TimeoutException;
import com.android.ide.eclipse.gltrace.editors.GLFunctionTraceViewer;
import com.google.common.io.Closeables;
import com.google.common.util.concurrent.SimpleTimeLimiter;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.runtime.Path;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.IURIEditorInput;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.IWorkbenchWindowActionDelegate;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.WorkbenchException;
import org.eclipse.ui.ide.IDE;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.concurrent.Callable;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class CollectTraceAction implements IWorkbenchWindowActionDelegate {
/** Abstract Unix Domain Socket Name used by the gltrace device code. */
private static final String GLTRACE_UDS = "gltrace"; //$NON-NLS-1$
/** Local port that is forwarded to the device's {@link #GLTRACE_UDS} socket. */
private static final int LOCAL_FORWARDED_PORT = 6039;
/** Activity name to use for a system activity that has already been launched. */
private static final String SYSTEM_APP = "system"; //$NON-NLS-1$
/** Time to wait for the application to launch (seconds) */
private static final int LAUNCH_TIMEOUT = 15;
/** Time to wait for the application to die (seconds) */
private static final int KILL_TIMEOUT = 5;
private static final int MIN_API_LEVEL = 16;
@Override
public void run(IAction action) {
connectToDevice();
}
@Override
public void selectionChanged(IAction action, ISelection selection) {
}
@Override
public void dispose() {
}
@Override
public void init(IWorkbenchWindow window) {
}
private void connectToDevice() {
Shell shell = Display.getDefault().getActiveShell();
GLTraceOptionsDialog dlg = new GLTraceOptionsDialog(shell);
if (dlg.open() != Window.OK) {
return;
}
TraceOptions traceOptions = dlg.getTraceOptions();
IDevice device = getDevice(traceOptions.device);
String apiLevelString = device.getProperty(IDevice.PROP_BUILD_API_LEVEL);
int apiLevel;
try {
apiLevel = Integer.parseInt(apiLevelString);
} catch (NumberFormatException e) {
apiLevel = MIN_API_LEVEL;
}
if (apiLevel < MIN_API_LEVEL) {
MessageDialog.openError(shell, "GL Trace",
String.format("OpenGL Tracing is only supported on devices at API Level %1$d."
+ "The selected device '%2$s' provides API level %3$s.",
MIN_API_LEVEL, traceOptions.device, apiLevelString));
return;
}
try {
setupForwarding(device, LOCAL_FORWARDED_PORT);
} catch (Exception e) {
MessageDialog.openError(shell, "Setup GL Trace",
"Error while setting up port forwarding: " + e.getMessage());
return;
}
try {
if (!SYSTEM_APP.equals(traceOptions.appToTrace)) {
startActivity(device, traceOptions.appToTrace, traceOptions.activityToTrace,
traceOptions.isActivityNameFullyQualified);
}
} catch (Exception e) {
MessageDialog.openError(shell, "Setup GL Trace",
"Error while launching application: " + e.getMessage());
return;
}
// if everything went well, the app should now be waiting for the gl debugger
// to connect
startTracing(shell, traceOptions, LOCAL_FORWARDED_PORT);
// once tracing is complete, remove port forwarding
disablePortForwarding(device, LOCAL_FORWARDED_PORT);
// and finally open the editor to view the file
openInEditor(shell, traceOptions.traceDestination);
}
public static void openInEditor(Shell shell, String traceFilePath) {
final IFileStore fileStore = EFS.getLocalFileSystem().getStore(new Path(traceFilePath));
if (!fileStore.fetchInfo().exists()) {
return;
}
final IWorkbench workbench = PlatformUI.getWorkbench();
IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
if (window == null) {
return;
}
IWorkbenchPage page = window.getActivePage();
if (page == null) {
return;
}
try {
workbench.showPerspective("com.android.ide.eclipse.gltrace.perspective", window);
} catch (WorkbenchException e) {
}
// if there is a editor already open, then refresh its model
GLFunctionTraceViewer viewer = getOpenTraceViewer(page, traceFilePath);
if (viewer != null) {
viewer.setInput(shell, traceFilePath);
}
// open the editor (if not open), or bring it to foreground if it is already open
try {
IDE.openEditorOnFileStore(page, fileStore);
} catch (PartInitException e) {
GlTracePlugin.getDefault().logMessage(
"Unexpected error while opening gltrace file in editor: " + e);
return;
}
}
/**
* Returns the editor part that has the provided file path open.
* @param page page containing editors
* @param traceFilePath file that should be open in an editor
* @return if given trace file is already open, then a reference to that editor part,
* null otherwise
*/
private static GLFunctionTraceViewer getOpenTraceViewer(IWorkbenchPage page,
String traceFilePath) {
IEditorReference[] editorRefs = page.getEditorReferences();
for (IEditorReference ref : editorRefs) {
String id = ref.getId();
if (!GLFunctionTraceViewer.ID.equals(id)) {
continue;
}
IEditorInput input = null;
try {
input = ref.getEditorInput();
} catch (PartInitException e) {
continue;
}
if (!(input instanceof IURIEditorInput)) {
continue;
}
if (traceFilePath.equals(((IURIEditorInput) input).getURI().getPath())) {
return (GLFunctionTraceViewer) ref.getEditor(true);
}
}
return null;
}
@SuppressWarnings("resource") // Closeables.closeQuietly
public static void startTracing(Shell shell, TraceOptions traceOptions, int port) {
Socket socket = new Socket();
DataInputStream traceDataStream = null;
DataOutputStream traceCommandsStream = null;
try {
socket.connect(new java.net.InetSocketAddress("127.0.0.1", port)); //$NON-NLS-1$
socket.setTcpNoDelay(true);
traceDataStream = new DataInputStream(socket.getInputStream());
traceCommandsStream = new DataOutputStream(socket.getOutputStream());
} catch (IOException e) {
MessageDialog.openError(shell,
"OpenGL Trace",
"Unable to connect to remote GL Trace Server: " + e.getMessage());
return;
}
// create channel to send trace commands to device
TraceCommandWriter traceCommandWriter = new TraceCommandWriter(traceCommandsStream);
try {
traceCommandWriter.setTraceOptions(traceOptions.collectFbOnEglSwap,
traceOptions.collectFbOnGlDraw,
traceOptions.collectTextureData);
} catch (IOException e) {
MessageDialog.openError(shell,
"OpenGL Trace",
"Unexpected error while setting trace options: " + e.getMessage());
closeSocket(socket);
return;
}
FileOutputStream fos = null;
try {
fos = new FileOutputStream(traceOptions.traceDestination, false);
} catch (FileNotFoundException e) {
// input path is valid, so this cannot occur
}
// create trace writer that writes to a trace file
TraceFileWriter traceFileWriter = new TraceFileWriter(fos, traceDataStream);
traceFileWriter.start();
GLTraceCollectorDialog dlg = new GLTraceCollectorDialog(shell,
traceFileWriter,
traceCommandWriter,
traceOptions);
dlg.open();
traceFileWriter.stopTracing();
traceCommandWriter.close();
closeSocket(socket);
}
private static void closeSocket(Socket socket) {
try {
socket.close();
} catch (IOException e) {
// ignore error while closing socket
}
}
private void startActivity(IDevice device, String appPackage, String activity,
boolean isActivityNameFullyQualified)
throws TimeoutException, AdbCommandRejectedException,
ShellCommandUnresponsiveException, IOException, InterruptedException {
killApp(device, appPackage); // kill app if it is already running
waitUntilAppKilled(device, appPackage, KILL_TIMEOUT);
StringBuilder activityPath = new StringBuilder(appPackage);
if (!activity.isEmpty()) {
activityPath.append('/');
if (!isActivityNameFullyQualified) {
activityPath.append('.');
}
activityPath.append(activity);
}
String startAppCmd = String.format(
"am start --opengl-trace %s -a android.intent.action.MAIN -c android.intent.category.LAUNCHER", //$NON-NLS-1$
activityPath.toString());
Semaphore launchCompletionSempahore = new Semaphore(0);
StartActivityOutputReceiver receiver = new StartActivityOutputReceiver(
launchCompletionSempahore);
device.executeShellCommand(startAppCmd, receiver);
// wait until shell finishes launch command
launchCompletionSempahore.acquire();
// throw exception if there was an error during launch
String output = receiver.getOutput();
if (output.contains("Error")) { //$NON-NLS-1$
throw new RuntimeException(output);
}
// wait until the app itself has been launched
waitUntilAppLaunched(device, appPackage, LAUNCH_TIMEOUT);
}
private void killApp(IDevice device, String appName) {
Client client = device.getClient(appName);
if (client != null) {
client.kill();
}
}
private void waitUntilAppLaunched(final IDevice device, final String appName, int timeout) {
Callable<Boolean> c = new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
Client client;
do {
client = device.getClient(appName);
} while (client == null);
return Boolean.TRUE;
}
};
try {
new SimpleTimeLimiter().callWithTimeout(c, timeout, TimeUnit.SECONDS, true);
} catch (Exception e) {
throw new RuntimeException("Timed out waiting for application to launch.");
}
// once the app has launched, wait an additional couple of seconds
// for it to start up
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// ignore
}
}
private void waitUntilAppKilled(final IDevice device, final String appName, int timeout) {
Callable<Boolean> c = new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
Client client;
while ((client = device.getClient(appName)) != null) {
client.kill();
}
return Boolean.TRUE;
}
};
try {
new SimpleTimeLimiter().callWithTimeout(c, timeout, TimeUnit.SECONDS, true);
} catch (Exception e) {
throw new RuntimeException("Timed out waiting for running application to die.");
}
}
public static void setupForwarding(IDevice device, int i)
throws TimeoutException, AdbCommandRejectedException, IOException {
device.createForward(i, GLTRACE_UDS, DeviceUnixSocketNamespace.ABSTRACT);
}
public static void disablePortForwarding(IDevice device, int port) {
try {
device.removeForward(port, GLTRACE_UDS, DeviceUnixSocketNamespace.ABSTRACT);
} catch (Exception e) {
// ignore exceptions;
}
}
private IDevice getDevice(String deviceName) {
IDevice[] devices = AndroidDebugBridge.getBridge().getDevices();
for (IDevice device : devices) {
if (device.getName().equals(deviceName)) {
return device;
}
}
return null;
}
private static class StartActivityOutputReceiver implements IShellOutputReceiver {
private Semaphore mSemaphore;
private StringBuffer sb = new StringBuffer(300);
public StartActivityOutputReceiver(Semaphore s) {
mSemaphore = s;
}
@Override
public void addOutput(byte[] data, int offset, int length) {
String d = new String(data, offset, length);
sb.append(d);
}
@Override
public void flush() {
mSemaphore.release();
}
@Override
public boolean isCancelled() {
return false;
}
public String getOutput() {
return sb.toString();
}
}
}