blob: d15ecb6c1ac90c8e56673d380fcc8caddde3e32a [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.*;
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;
}
}