| /* |
| * 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.ddmlib.Client; |
| 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.DefaultProgramRunner; |
| import com.intellij.execution.runners.ExecutionEnvironment; |
| import com.intellij.execution.runners.ExecutionEnvironmentBuilder; |
| import com.intellij.execution.runners.ProgramRunner; |
| 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 com.android.tools.idea.monitor.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 (!(environment.getExecutor() instanceof DefaultDebugExecutor)) { |
| 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().requiresAndroidModel()) { |
| 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) { |
| if (!DefaultDebugExecutor.EXECUTOR_ID.equals(executorId) && !DefaultRunExecutor.EXECUTOR_ID.equals(executorId)) { |
| return false; |
| } |
| |
| if (!(profile instanceof AndroidRunConfigurationBase)) { |
| return false; |
| } |
| |
| return ((AndroidRunConfigurationBase)profile).usesSimpleLauncher(); |
| } |
| |
| 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(@NotNull final Client client) { |
| ApplicationManager.getApplication().invokeLater(new Runnable() { |
| @Override |
| @SuppressWarnings({"IOResourceOpenedButNotSafelyClosed"}) |
| public void run() { |
| IDevice device = client.getDevice(); |
| String debugPort = Integer.toString(client.getDebuggerListenPort()); |
| |
| 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"); |
| } |
| }); |
| } |
| } |
| } |