| /* |
| * 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.AndroidDebugState; |
| import com.android.tools.idea.run.AndroidProcessText; |
| import com.android.tools.idea.run.AndroidRunConfigurationBase; |
| import com.android.tools.idea.run.AndroidSessionInfo; |
| import com.android.tools.idea.run.ApkProvisionException; |
| import com.android.tools.idea.run.ApplicationIdProvider; |
| import com.android.tools.idea.run.LaunchInfo; |
| import com.android.tools.idea.run.ProcessHandlerConsolePrinter; |
| import com.android.tools.idea.run.editor.AndroidDebugger; |
| import com.android.tools.idea.run.tasks.ConnectJavaDebuggerTask; |
| import com.android.tools.idea.run.util.ProcessHandlerLaunchStatus; |
| import com.google.idea.blaze.android.compatibility.Compatibility.ConnectDebuggerTask; |
| 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 java.util.Locale; |
| import java.util.Set; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| /** 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, true); |
| 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 |
| ProcessHandler oldProcessHandler = launchStatus.getProcessHandler(); |
| launchStatus.setProcessHandler(debugProcessHandler); |
| printer.setProcessHandler(debugProcessHandler); |
| |
| // Detach old process handler after the launch status |
| // has been updated to point to the new process handler. |
| oldProcessHandler.detachProcess(); |
| |
| AndroidDebugState debugState = |
| new AndroidDebugState( |
| project, debugProcessHandler, connection, currentLaunchInfo.consoleProvider); |
| |
| RunContentDescriptor oldDescriptor; |
| AndroidSessionInfo oldSession = oldProcessHandler.getUserData(AndroidSessionInfo.KEY); |
| if (oldSession != null) { |
| oldDescriptor = oldSession.getDescriptor(); |
| } else { |
| // This is the first time we are attaching the debugger; get it from the environment instead. |
| oldDescriptor = currentLaunchInfo.env.getContentToReuse(); |
| } |
| |
| RunContentDescriptor debugDescriptor; |
| try { |
| // @formatter:off |
| ExecutionEnvironment debugEnv = |
| new ExecutionEnvironmentBuilder(currentLaunchInfo.env) |
| .executor(currentLaunchInfo.executor) |
| .runner(currentLaunchInfo.runner) |
| .contentToReuse(oldDescriptor) |
| .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? |
| final AndroidProcessText oldText = AndroidProcessText.get(oldProcessHandler); |
| 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; |
| } |
| } |