/* * Copyright 2000-2010 JetBrains s.r.o. * * 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 org.jetbrains.android.run; import com.android.annotations.concurrency.GuardedBy; import com.android.ddmlib.IDevice; import com.intellij.debugger.engine.RemoteDebugProcessHandler; import com.intellij.debugger.ui.DebuggerPanelsManager; import com.intellij.execution.*; import com.intellij.execution.configurations.RemoteConnection; import com.intellij.execution.configurations.RemoteState; import com.intellij.execution.configurations.RunProfile; import com.intellij.execution.configurations.RunProfileState; import com.intellij.execution.executors.DefaultDebugExecutor; import com.intellij.execution.executors.DefaultRunExecutor; import com.intellij.execution.process.ProcessAdapter; import com.intellij.execution.process.ProcessEvent; import com.intellij.execution.process.ProcessHandler; import com.intellij.execution.runners.*; import com.intellij.execution.ui.ConsoleView; import com.intellij.execution.ui.RunContentDescriptor; import com.intellij.execution.ui.RunContentManager; import com.intellij.notification.Notification; import com.intellij.notification.NotificationGroup; import com.intellij.notification.NotificationListener; import com.intellij.notification.NotificationType; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.Ref; import com.intellij.openapi.wm.ToolWindow; import com.intellij.openapi.wm.ToolWindowManager; import com.intellij.psi.PsiClass; import com.intellij.ui.content.Content; import com.intellij.xdebugger.DefaultDebugProcessHandler; import org.jetbrains.android.dom.manifest.Instrumentation; import org.jetbrains.android.dom.manifest.Manifest; import org.jetbrains.android.logcat.AndroidToolWindowFactory; import org.jetbrains.android.run.testing.AndroidTestRunConfiguration; import org.jetbrains.android.util.AndroidBundle; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.event.HyperlinkEvent; import java.util.List; import static com.intellij.execution.process.ProcessOutputTypes.STDERR; import static com.intellij.execution.process.ProcessOutputTypes.STDOUT; /** * @author coyote */ public class AndroidDebugRunner extends DefaultProgramRunner { public static final Key<AndroidSessionInfo> ANDROID_SESSION_INFO = new Key<AndroidSessionInfo>("ANDROID_SESSION_INFO"); private static final Object myDebugLock = new Object(); public static final String ANDROID_LOGCAT_CONTENT_ID = "Android Logcat"; private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.run.AndroidDebugRunner"); private static NotificationGroup ourNotificationGroup; // created and accessed only in EDT private static void tryToCloseOldSessions(final Executor executor, Project project) { final ExecutionManager manager = ExecutionManager.getInstance(project); ProcessHandler[] processes = manager.getRunningProcesses(); for (ProcessHandler process : processes) { final AndroidSessionInfo info = process.getUserData(ANDROID_SESSION_INFO); if (info != null) { process.addProcessListener(new ProcessAdapter() { @Override public void processTerminated(ProcessEvent event) { ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { manager.getContentManager().removeRunContent(executor, info.getDescriptor()); } }); } }); process.detachProcess(); } } } @Override protected RunContentDescriptor doExecute(@NotNull final RunProfileState state, @NotNull final ExecutionEnvironment environment) throws ExecutionException { assert state instanceof AndroidRunningState; final AndroidRunningState runningState = (AndroidRunningState)state; final RunContentDescriptor[] descriptor = {null}; runningState.addListener(new AndroidRunningStateListener() { @Override public void executionFailed() { ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { if (descriptor[0] != null) { showNotification(environment.getProject(), environment.getExecutor(), descriptor[0], "error", false, NotificationType.ERROR); } } }); } }); descriptor[0] = doExec(runningState, environment); return descriptor[0]; } private RunContentDescriptor doExec(AndroidRunningState state, ExecutionEnvironment environment) throws ExecutionException { if (DefaultRunExecutor.EXECUTOR_ID.equals(environment.getExecutor().getId())) { final RunContentDescriptor descriptor = super.doExecute(state, environment); if (descriptor != null) { setActivateToolWindowWhenAddedProperty(environment.getProject(), environment.getExecutor(), descriptor, "running"); } return descriptor; } final RunProfile runProfile = environment.getRunProfile(); if (runProfile instanceof AndroidTestRunConfiguration) { // attempt to set the target package only in case on non Gradle projects if (!state.getFacet().isGradleProject()) { String targetPackage = getTargetPackage((AndroidTestRunConfiguration)runProfile, state); if (targetPackage == null) { throw new ExecutionException(AndroidBundle.message("target.package.not.specified.error")); } state.setTargetPackageName(targetPackage); } } state.setDebugMode(true); RunContentDescriptor runDescriptor; synchronized (myDebugLock) { MyDebugLauncher launcher = new MyDebugLauncher(state, environment); state.setDebugLauncher(launcher); final RunContentDescriptor descriptor = embedToExistingSession(environment.getProject(), environment.getExecutor(), state); runDescriptor = descriptor != null ? descriptor : super.doExecute(state, environment); launcher.setRunDescriptor(runDescriptor); if (descriptor != null) { return null; } } if (runDescriptor == null) { return null; } tryToCloseOldSessions(environment.getExecutor(), environment.getProject()); final ProcessHandler handler = state.getProcessHandler(); handler.putUserData(ANDROID_SESSION_INFO, new AndroidSessionInfo(runDescriptor, state, environment.getExecutor().getId())); setActivateToolWindowWhenAddedProperty(environment.getProject(), environment.getExecutor(), runDescriptor, "running"); return runDescriptor; } private static void setActivateToolWindowWhenAddedProperty(Project project, Executor executor, RunContentDescriptor descriptor, String status) { final boolean activateToolWindow = shouldActivateExecWindow(project); descriptor.setActivateToolWindowWhenAdded(activateToolWindow); if (!activateToolWindow) { showNotification(project, executor, descriptor, status, false, NotificationType.INFORMATION); } } private static boolean shouldActivateExecWindow(Project project) { final ToolWindow toolWindow = ToolWindowManager.getInstance(project).getToolWindow( AndroidToolWindowFactory.TOOL_WINDOW_ID); return toolWindow == null || !toolWindow.isVisible(); } @Nullable private static Pair<ProcessHandler, AndroidSessionInfo> findOldSession(Project project, Executor executor, AndroidRunConfigurationBase configuration) { for (ProcessHandler handler : ExecutionManager.getInstance(project).getRunningProcesses()) { final AndroidSessionInfo info = handler.getUserData(ANDROID_SESSION_INFO); if (info != null && info.getState().getConfiguration().equals(configuration) && executor.getId().equals(info.getExecutorId())) { return Pair.create(handler, info); } } return null; } @Nullable protected static RunContentDescriptor embedToExistingSession(final Project project, final Executor executor, final AndroidRunningState state) { final Pair<ProcessHandler, AndroidSessionInfo> pair = findOldSession(project, executor, state.getConfiguration()); final AndroidSessionInfo oldSessionInfo = pair != null ? pair.getSecond() : null; final ProcessHandler oldProcessHandler = pair != null ? pair.getFirst() : null; if (oldSessionInfo == null || oldProcessHandler == null) { return null; } final AndroidExecutionState oldState = oldSessionInfo.getState(); final IDevice[] oldDevices = oldState.getDevices(); final ConsoleView oldConsole = oldState.getConsoleView(); if (oldDevices == null || oldConsole == null || oldDevices.length == 0 || oldDevices.length > 1) { return null; } final Ref<List<IDevice>> devicesRef = Ref.create(); final boolean result = ProgressManager.getInstance().runProcessWithProgressSynchronously(new Runnable() { @Override public void run() { devicesRef.set(state.getAllCompatibleDevices()); } }, "Scanning available devices", false, project); if (!result) { return null; } final List<IDevice> devices = devicesRef.get(); if (devices.size() == 0 || devices.size() > 1 || devices.get(0) != oldDevices[0]) { return null; } oldProcessHandler.detachProcess(); state.setTargetDevices(devices.toArray(new IDevice[devices.size()])); state.setConsole(oldConsole); final RunContentDescriptor oldDescriptor = oldSessionInfo.getDescriptor(); ProcessHandler newProcessHandler; if (oldDescriptor.getProcessHandler() instanceof RemoteDebugProcessHandler) { newProcessHandler = oldDescriptor.getProcessHandler(); newProcessHandler.destroyProcess(); } else { newProcessHandler = new DefaultDebugProcessHandler(); } oldDescriptor.setProcessHandler(newProcessHandler); state.setProcessHandler(newProcessHandler); oldConsole.attachToProcess(newProcessHandler); AndroidProcessText.attach(newProcessHandler); newProcessHandler.notifyTextAvailable("The session was restarted\n", STDOUT); showNotification(project, executor, oldDescriptor, "running", false, NotificationType.INFORMATION); state.addListener(new AndroidRunningStateListener() { @Override public void executionFailed() { ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { showNotification(project, executor, oldDescriptor, "error", false, NotificationType.ERROR); } }); } }); ApplicationManager.getApplication().executeOnPooledThread(new Runnable() { @Override public void run() { state.start(false); } }); return oldDescriptor; } private static void showNotification(final Project project, final Executor executor, final RunContentDescriptor descriptor, final String status, final boolean notifySelectedContent, final NotificationType type) { ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { if (project.isDisposed()) { return; } final String sessionName = descriptor.getDisplayName(); final ToolWindow toolWindow = ToolWindowManager.getInstance(project).getToolWindow(executor.getToolWindowId()); final Content content = descriptor.getAttachedContent(); final String notificationMessage; if (content != null && content.isSelected() && toolWindow.isVisible()) { if (!notifySelectedContent) { return; } notificationMessage = "Session '" + sessionName + "': " + status; } else { notificationMessage = "Session <a href=''>'" + sessionName + "'</a>: " + status; } if (ourNotificationGroup == null) { ourNotificationGroup = NotificationGroup.toolWindowGroup("Android Session Restarted", executor.getToolWindowId()); } ourNotificationGroup .createNotification("", notificationMessage, type, new NotificationListener() { @Override public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) { if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { final RunContentManager contentManager = ExecutionManager.getInstance(project).getContentManager(); for (RunContentDescriptor d : contentManager.getAllDescriptors()) { if (sessionName.equals(d.getDisplayName())) { final Content content = d.getAttachedContent(); content.getManager().setSelectedContent(content); toolWindow.activate(null, true, true); break; } } } } }).notify(project); } }); } @Nullable private static String getTargetPackage(AndroidTestRunConfiguration configuration, AndroidRunningState state) { Manifest manifest = state.getFacet().getManifest(); assert manifest != null; for (Instrumentation instrumentation : manifest.getInstrumentations()) { PsiClass c = instrumentation.getInstrumentationClass().getValue(); String runner = configuration.INSTRUMENTATION_RUNNER_CLASS; if (c != null && (runner.length() == 0 || runner.equals(c.getQualifiedName()))) { String targetPackage = instrumentation.getTargetPackage().getValue(); if (targetPackage != null) { return targetPackage; } } } return null; } private static class AndroidDebugState implements RemoteState, AndroidExecutionState { private final Project myProject; private final RemoteConnection myConnection; private final AndroidRunningState myState; private final IDevice myDevice; private volatile ConsoleView myConsoleView; public AndroidDebugState(Project project, RemoteConnection connection, AndroidRunningState state, IDevice device) { myProject = project; myConnection = connection; myState = state; myDevice = device; } @Override public ExecutionResult execute(final Executor executor, @NotNull final ProgramRunner runner) throws ExecutionException { RemoteDebugProcessHandler process = new RemoteDebugProcessHandler(myProject); myState.setProcessHandler(process); myConsoleView = myState.getConfiguration().attachConsole(myState, executor); final LogcatExecutionConsole console = new LogcatExecutionConsole(myProject, myDevice, myConsoleView, myState.getConfiguration().getType().getId()); return new DefaultExecutionResult(console, process); } @Override public RemoteConnection getRemoteConnection() { return myConnection; } @Override public IDevice[] getDevices() { return new IDevice[]{myDevice}; } @Nullable @Override public ConsoleView getConsoleView() { return myConsoleView; } @NotNull @Override public AndroidRunConfigurationBase getConfiguration() { return myState.getConfiguration(); } } @Override @NotNull public String getRunnerId() { return "AndroidDebugRunner"; } @Override public boolean canRun(@NotNull String executorId, @NotNull RunProfile profile) { return (DefaultDebugExecutor.EXECUTOR_ID.equals(executorId) || DefaultRunExecutor.EXECUTOR_ID.equals(executorId)) && profile instanceof AndroidRunConfigurationBase; } private class MyDebugLauncher implements DebugLauncher { private final Project myProject; private final Executor myExecutor; private final AndroidRunningState myRunningState; private final ExecutionEnvironment myEnvironment; private RunContentDescriptor myRunDescriptor; public MyDebugLauncher(AndroidRunningState state, ExecutionEnvironment environment) { myProject = environment.getProject(); myRunningState = state; myEnvironment = environment; myExecutor = environment.getExecutor(); } public void setRunDescriptor(RunContentDescriptor runDescriptor) { myRunDescriptor = runDescriptor; } @Override public void launchDebug(final IDevice device, final String debugPort) { ApplicationManager.getApplication().invokeLater(new Runnable() { @Override @SuppressWarnings({"IOResourceOpenedButNotSafelyClosed"}) public void run() { final DebuggerPanelsManager manager = DebuggerPanelsManager.getInstance(myProject); AndroidDebugState st = new AndroidDebugState(myProject, new RemoteConnection(true, "localhost", debugPort, false), myRunningState, device); RunContentDescriptor debugDescriptor = null; final ProcessHandler processHandler = myRunningState.getProcessHandler(); processHandler.detachProcess(); try { synchronized (myDebugLock) { assert myRunDescriptor != null; debugDescriptor = manager.attachVirtualMachine(new ExecutionEnvironmentBuilder(myEnvironment) .executor(myExecutor) .runner(AndroidDebugRunner.this) .contentToReuse(myRunDescriptor) .build(), st, st.getRemoteConnection(), false); } } catch (ExecutionException e) { processHandler.notifyTextAvailable("ExecutionException: " + e.getMessage() + '.', STDERR); } ProcessHandler newProcessHandler = debugDescriptor != null ? debugDescriptor.getProcessHandler() : null; if (debugDescriptor == null || newProcessHandler == null) { LOG.info("cannot start debugging"); return; } final AndroidProcessText oldText = AndroidProcessText.get(processHandler); if (oldText != null) { oldText.printTo(newProcessHandler); } AndroidProcessText.attach(newProcessHandler); myRunningState.getProcessHandler().putUserData(ANDROID_SESSION_INFO, new AndroidSessionInfo( debugDescriptor, st, myExecutor.getId())); setActivateToolWindowWhenAddedProperty(myProject, myExecutor, debugDescriptor, "debugger connected"); } }); } } }