| /* |
| * Copyright 2016 The Bazel Authors. All rights reserved. |
| * |
| * 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.google.idea.blaze.android.run.test; |
| |
| import com.android.ddmlib.AndroidDebugBridge; |
| import com.android.ddmlib.Client; |
| import com.android.ddmlib.ClientData; |
| import com.android.ddmlib.IDevice; |
| import com.android.tools.idea.run.*; |
| import com.android.tools.idea.run.editor.AndroidDebugger; |
| import com.android.tools.idea.run.tasks.ConnectDebuggerTask; |
| import com.android.tools.idea.run.tasks.ConnectJavaDebuggerTask; |
| import com.android.tools.idea.run.util.ProcessHandlerLaunchStatus; |
| import com.intellij.debugger.engine.RemoteDebugProcessHandler; |
| import com.intellij.debugger.ui.DebuggerPanelsManager; |
| import com.intellij.execution.ExecutionException; |
| import com.intellij.execution.configurations.RemoteConnection; |
| import com.intellij.execution.configurations.RunProfile; |
| import com.intellij.execution.process.ProcessHandler; |
| import com.intellij.execution.runners.ExecutionEnvironment; |
| import com.intellij.execution.runners.ExecutionEnvironmentBuilder; |
| import com.intellij.execution.ui.RunContentDescriptor; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.project.Project; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.Locale; |
| import java.util.Set; |
| |
| /** |
| * Connects the blaze debugger during execution. |
| */ |
| class ConnectBlazeTestDebuggerTask extends ConnectDebuggerTask { |
| private static final Logger LOG = Logger.getInstance(ConnectBlazeTestDebuggerTask.class); |
| |
| private final Project project; |
| private final ApplicationIdProvider applicationIdProvider; |
| private final BlazeAndroidTestRunContext runContext; |
| |
| public ConnectBlazeTestDebuggerTask( |
| Project project, |
| AndroidDebugger debugger, |
| Set<String> applicationIds, |
| ApplicationIdProvider applicationIdProvider, |
| BlazeAndroidTestRunContext runContext) { |
| super(applicationIds, debugger, project); |
| this.project = project; |
| this.applicationIdProvider = applicationIdProvider; |
| this.runContext = runContext; |
| } |
| |
| @Nullable |
| @Override |
| public ProcessHandler perform(@NotNull LaunchInfo launchInfo, |
| @NotNull IDevice device, |
| @NotNull ProcessHandlerLaunchStatus state, |
| @NotNull ProcessHandlerConsolePrinter printer) { |
| try { |
| String packageName = applicationIdProvider.getPackageName(); |
| setUpForReattachingDebugger(packageName, launchInfo, state, printer); |
| } |
| catch (ApkProvisionException e) { |
| LOG.error(e); |
| } |
| |
| // The return value for this task is not used |
| return null; |
| } |
| |
| /** |
| * Wires up listeners to automatically reconnect the debugger for each test method. |
| * When you `blaze test` an android_test in debug mode, it kills the instrumentation process |
| * between each test method, disconnecting the debugger. We listen for the start of a new |
| * method waiting for a debugger, and reconnect. |
| * TODO: Support stopping Blaze from the UI. This is hard because we have no way to distinguish |
| * process handler termination/debug session ending initiated by the user. |
| */ |
| private void setUpForReattachingDebugger( |
| String targetPackage, |
| LaunchInfo launchInfo, |
| ProcessHandlerLaunchStatus launchStatus, |
| ProcessHandlerConsolePrinter printer |
| ) { |
| final AndroidDebugBridge.IClientChangeListener reattachingListener = |
| new AndroidDebugBridge.IClientChangeListener() { |
| // The target application can either |
| // 1. Match our target name, and become available for debugging. |
| // 2. Be available for debugging, and suddenly have its name changed to match. |
| static final int CHANGE_MASK = Client.CHANGE_DEBUGGER_STATUS | Client.CHANGE_NAME; |
| |
| @Override |
| public void clientChanged(@NotNull Client client, int changeMask) { |
| ClientData data = client.getClientData(); |
| String clientDescription = data.getClientDescription(); |
| if (clientDescription != null && clientDescription.equals(targetPackage) |
| && (changeMask & CHANGE_MASK) != 0 |
| && data.getDebuggerConnectionStatus().equals(ClientData.DebuggerStatus.WAITING)) { |
| reattachDebugger(launchInfo, client, launchStatus, printer); |
| } |
| } |
| }; |
| |
| AndroidDebugBridge.addClientChangeListener(reattachingListener); |
| runContext.addLaunchTaskCompleteListener(() -> { |
| AndroidDebugBridge.removeClientChangeListener(reattachingListener); |
| launchStatus.terminateLaunch("Test run completed.\n"); |
| }); |
| } |
| |
| private void reattachDebugger( |
| LaunchInfo launchInfo, |
| final Client client, |
| ProcessHandlerLaunchStatus launchStatus, |
| ProcessHandlerConsolePrinter printer |
| ) { |
| ApplicationManager.getApplication().invokeLater(() -> launchDebugger(launchInfo, client, launchStatus, printer)); |
| } |
| |
| /** |
| * Nearly a clone of {@link ConnectJavaDebuggerTask#launchDebugger}. There are a few changes to account for null variables that could |
| * occur in our implementation. |
| */ |
| @Override |
| public ProcessHandler launchDebugger(@NotNull LaunchInfo currentLaunchInfo, |
| @NotNull Client client, |
| @NotNull ProcessHandlerLaunchStatus launchStatus, |
| @NotNull ProcessHandlerConsolePrinter printer) { |
| String debugPort = Integer.toString(client.getDebuggerListenPort()); |
| int pid = client.getClientData().getPid(); |
| Logger.getInstance(ConnectJavaDebuggerTask.class) |
| .info(String.format(Locale.US, "Attempting to connect debugger to port %1$s [client %2$d]", debugPort, pid)); |
| |
| // create a new process handler |
| RemoteConnection connection = new RemoteConnection(true, "localhost", debugPort, false); |
| RemoteDebugProcessHandler debugProcessHandler = new RemoteDebugProcessHandler(project); |
| |
| // switch the launch status and console printers to point to the new process handler |
| // this is required, esp. for AndroidTestListener which holds a reference to the launch status and printers, and those should |
| // be updated to point to the new process handlers, otherwise test results will not be forwarded appropriately |
| launchStatus.setProcessHandler(debugProcessHandler); |
| printer.setProcessHandler(debugProcessHandler); |
| |
| // detach old process handler |
| RunContentDescriptor descriptor = currentLaunchInfo.env.getContentToReuse(); |
| assert descriptor != null; |
| |
| final ProcessHandler processHandler = descriptor.getProcessHandler(); |
| |
| // detach after the launch status has been updated to point to the new process handler |
| if (processHandler != null) { |
| processHandler.detachProcess(); |
| } |
| |
| AndroidDebugState debugState = new AndroidDebugState(project, debugProcessHandler, connection, currentLaunchInfo.consoleProvider); |
| |
| RunContentDescriptor debugDescriptor; |
| try { |
| // @formatter:off |
| ExecutionEnvironment debugEnv = new ExecutionEnvironmentBuilder(currentLaunchInfo.env) |
| .executor(currentLaunchInfo.executor) |
| .runner(currentLaunchInfo.runner) |
| .contentToReuse(processHandler == null ? null : descriptor) |
| .build(); |
| debugDescriptor = DebuggerPanelsManager.getInstance(project).attachVirtualMachine(debugEnv, debugState, connection, false); |
| // @formatter:on |
| } |
| catch (ExecutionException e) { |
| printer.stderr("ExecutionException: " + e.getMessage() + '.'); |
| return null; |
| } |
| |
| // Based on the above try block, we shouldn't get here unless we have assigned to debugDescriptor |
| assert debugDescriptor != null; |
| |
| // re-run the collected text from the old process handler to the new |
| // TODO: is there a race between messages received once the debugger has been connected, and these messages that are printed out? |
| if (processHandler != null) { |
| final AndroidProcessText oldText = AndroidProcessText.get(processHandler); |
| if (oldText != null) { |
| oldText.printTo(debugProcessHandler); |
| } |
| } |
| |
| RunProfile runProfile = currentLaunchInfo.env.getRunProfile(); |
| int uniqueId = runProfile instanceof AndroidRunConfigurationBase ? ((AndroidRunConfigurationBase)runProfile).getUniqueID() : -1; |
| AndroidSessionInfo value = |
| new AndroidSessionInfo(debugProcessHandler, debugDescriptor, uniqueId, currentLaunchInfo.executor.getId(), false); |
| debugProcessHandler.putUserData(AndroidSessionInfo.KEY, value); |
| debugProcessHandler.putUserData(AndroidSessionInfo.ANDROID_DEBUG_CLIENT, client); |
| debugProcessHandler.putUserData(AndroidSessionInfo.ANDROID_DEVICE_API_LEVEL, client.getDevice().getVersion()); |
| |
| return debugProcessHandler; |
| } |
| } |