blob: 8cdeedf7abcc1c3b32f092dd4fd2476bac1caf4c [file] [log] [blame]
/*
* 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.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 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;
}
}