blob: eed693ed454d087e8328d6784aaf8325d6904f60 [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.runner;
import com.android.ddmlib.IDevice;
import com.android.tools.idea.run.*;
import com.android.tools.idea.run.editor.DeployTarget;
import com.android.tools.idea.run.editor.DeployTargetState;
import com.android.tools.idea.run.tasks.LaunchTasksProvider;
import com.android.tools.idea.run.util.LaunchUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.idea.blaze.android.run.BlazeAndroidRunConfiguration;
import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
import com.google.idea.blaze.base.command.BlazeFlags;
import com.google.idea.blaze.base.experiments.ExperimentScope;
import com.google.idea.blaze.base.metrics.Action;
import com.google.idea.blaze.base.projectview.ProjectViewManager;
import com.google.idea.blaze.base.projectview.ProjectViewSet;
import com.google.idea.blaze.base.scope.Scope;
import com.google.idea.blaze.base.scope.output.IssueOutput;
import com.google.idea.blaze.base.scope.scopes.BlazeConsoleScope;
import com.google.idea.blaze.base.scope.scopes.IssuesScope;
import com.google.idea.blaze.base.scope.scopes.LoggedTimingScope;
import com.google.idea.blaze.base.settings.BlazeUserSettings;
import com.intellij.execution.DefaultExecutionResult;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.ExecutionResult;
import com.intellij.execution.Executor;
import com.intellij.execution.configurations.RunProfileState;
import com.intellij.execution.executors.DefaultDebugExecutor;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.execution.runners.ProgramRunner;
import com.intellij.execution.ui.ConsoleView;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.JDOMExternalizable;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.WriteExternalException;
import org.jdom.Element;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.android.util.AndroidBundle;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import static com.android.tools.idea.gradle.util.Projects.requiredAndroidModelMissing;
import static org.jetbrains.android.actions.RunAndroidAvdManagerAction.getName;
/**
* Supports the entire run configuration flow. Used by both android_binary and android_test.
*
* Does any verification necessary, builds the APK and installs it, launches and debug tasks, etc.
*
* Any indirection between android_binary/android_test, mobile-install, InstantRun etc. should
* come via the strategy class.
*/
public final class BlazeAndroidRunConfigurationRunner implements JDOMExternalizable {
private static final Logger LOG = Logger.getInstance(BlazeAndroidRunConfigurationRunner.class);
private static final String SYNC_FAILED_ERR_MSG = "Project state is invalid. Please sync and try your action again.";
public static final Key<BlazeAndroidRunContext> RUN_CONTEXT_KEY = Key.create("blaze.run.context");
public static final Key<BlazeAndroidDeviceSelector.DeviceSession> DEVICE_SESSION_KEY = Key.create("blaze.device.session");
// We need to split "-c dbg" into two flags because we pass flags as a list of strings to the command line executor and we need blaze
// to see -c and dbg as two separate entities, not one.
private static final ImmutableList<String> NATIVE_DEBUG_FLAGS = ImmutableList.of("--fission=no", "-c", "dbg");
private final Project project;
private final BlazeAndroidRunConfiguration runConfiguration;
private final BlazeAndroidRunConfigurationCommonState commonState;
private final int runConfigId;
private final BlazeAndroidRunConfigurationDeployTargetManager deployTargetManager;
private final BlazeAndroidRunConfigurationDebuggerManager debuggerManager;
public BlazeAndroidRunConfigurationRunner(Project project,
BlazeAndroidRunConfiguration runConfiguration,
BlazeAndroidRunConfigurationCommonState commonState,
boolean isAndroidTest,
int runConfigId) {
this.project = project;
this.runConfiguration = runConfiguration;
this.commonState = commonState;
this.runConfigId = runConfigId;
this.deployTargetManager = new BlazeAndroidRunConfigurationDeployTargetManager(runConfigId, isAndroidTest);
this.debuggerManager = new BlazeAndroidRunConfigurationDebuggerManager(project, commonState);
}
private ImmutableList<String> getBuildFlags(Project project, ProjectViewSet projectViewSet) {
return ImmutableList.<String>builder()
.addAll(BlazeFlags.buildFlags(project, projectViewSet))
.addAll(commonState.getUserFlags())
.addAll(getNativeDebuggerFlags())
.build();
}
public ImmutableList<String> getNativeDebuggerFlags() {
return commonState.isNativeDebuggingEnabled() ? NATIVE_DEBUG_FLAGS : ImmutableList.of();
}
/**
* We collect errors rather than throwing to avoid missing fatal errors by exiting early for a warning.
* We use a separate method for the collection so the compiler prevents us from accidentally throwing.
*/
public List<ValidationError> validate(@Nullable Module module) {
List<ValidationError> errors = Lists.newArrayList();
if (module == null) {
errors.add(ValidationError.fatal("No run configuration module found"));
return errors;
}
if (commonState.getTarget() == null) {
errors.add(ValidationError.fatal("No target selected"));
return errors;
}
final Project project = module.getProject();
if (requiredAndroidModelMissing(project)) {
errors.add(ValidationError.fatal(SYNC_FAILED_ERR_MSG));
}
AndroidFacet facet = AndroidFacet.getInstance(module);
if (facet == null) {
// Can't proceed.
return ImmutableList.of(ValidationError.fatal(AndroidBundle.message("no.facet.error", module.getName())));
}
if (facet.getConfiguration().getAndroidPlatform() == null) {
errors.add(ValidationError.fatal(AndroidBundle.message("select.platform.error")));
}
errors.addAll(deployTargetManager.validate(facet));
errors.addAll(debuggerManager.validate(facet));
return errors;
}
@Nullable
public final RunProfileState getState(Module module,
final Executor executor,
ExecutionEnvironment env) throws ExecutionException {
assert module != null : "Enforced by fatal validation check in checkConfiguration.";
final AndroidFacet facet = AndroidFacet.getInstance(module);
assert facet != null : "Enforced by fatal validation check in checkConfiguration.";
Project project = env.getProject();
ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
if (projectViewSet == null) {
throw new ExecutionException("Could not load project view. Please resync project");
}
ImmutableList<String> buildFlags = getBuildFlags(project, projectViewSet);
BlazeAndroidRunContext runContext = runConfiguration.createRunContext(
project,
facet,
env,
buildFlags
);
runContext.augmentEnvironment(env);
boolean debug = executor instanceof DefaultDebugExecutor;
if (debug && !AndroidSdkUtils.activateDdmsIfNecessary(facet.getModule().getProject())) {
throw new ExecutionException("Unable to obtain debug bridge. Please check if there is a different tool using adb that is active.");
}
AndroidSessionInfo info = AndroidSessionInfo.findOldSession(project, null, runConfigId);
BlazeAndroidDeviceSelector deviceSelector = runContext.getDeviceSelector();
BlazeAndroidDeviceSelector.DeviceSession deviceSession = deviceSelector.getDevice(
project,
facet,
deployTargetManager,
executor,
env,
info,
debug,
runConfigId
);
if (deviceSession == null) {
return null;
}
DeployTarget deployTarget = deviceSession.deployTarget;
if (deployTarget != null && deployTarget.hasCustomRunProfileState(executor)) {
DeployTargetState deployTargetState = deployTargetManager.getCurrentDeployTargetState();
return deployTarget.getRunProfileState(executor, env, deployTargetState);
}
DeviceFutures deviceFutures = deviceSession.deviceFutures;
if (deviceFutures == null) {
// The user deliberately canceled, or some error was encountered and exposed by the chooser. Quietly exit.
return null;
}
if (deviceFutures.get().isEmpty()) {
throw new ExecutionException(AndroidBundle.message("deployment.target.not.found"));
}
if (debug) {
String error = canDebug(deviceFutures, facet, module.getName());
if (error != null) {
throw new ExecutionException(error);
}
}
LaunchOptions.Builder launchOptionsBuilder = getDefaultLaunchOptions()
.setDebug(debug);
runContext.augmentLaunchOptions(launchOptionsBuilder);
LaunchOptions launchOptions = launchOptionsBuilder.build();
// Store the run context on the execution environment so before-run tasks can access it.
env.putCopyableUserData(RUN_CONTEXT_KEY, runContext);
env.putCopyableUserData(DEVICE_SESSION_KEY, deviceSession);
return new BlazeAndroidRunState(
module,
env,
getName(),
launchOptions,
deviceSession,
runContext
);
}
private static String canDebug(DeviceFutures deviceFutures, AndroidFacet facet, String moduleName) {
// If we are debugging on a device, then the app needs to be debuggable
for (ListenableFuture<IDevice> future : deviceFutures.get()) {
if (!future.isDone()) {
// this is an emulator, and we assume that all emulators are debuggable
continue;
}
IDevice device = Futures.getUnchecked(future);
if (!LaunchUtils.canDebugAppOnDevice(facet, device)) {
return AndroidBundle.message("android.cannot.debug.noDebugPermissions", moduleName, device.getName());
}
}
return null;
}
private static LaunchOptions.Builder getDefaultLaunchOptions() {
return LaunchOptions.builder()
.setClearLogcatBeforeStart(false)
.setSkipNoopApkInstallations(true)
.setForceStopRunningApp(true);
}
public boolean executeBuild(ExecutionEnvironment env) {
boolean suppressConsole = BlazeUserSettings.getInstance().getSuppressConsoleForRunAction();
return Scope.root(context -> {
context
.push(new IssuesScope(project))
.push(new ExperimentScope())
.push(new BlazeConsoleScope.Builder(project).setSuppressConsole(suppressConsole).build())
.push(new LoggedTimingScope(project, Action.APK_BUILD_AND_INSTALL))
;
BlazeAndroidRunContext runContext = env.getCopyableUserData(RUN_CONTEXT_KEY);
if (runContext == null) {
IssueOutput.error("Could not find run context. Please try again").submit(context);
return false;
}
BlazeAndroidDeviceSelector.DeviceSession deviceSession = env.getCopyableUserData(DEVICE_SESSION_KEY);
BlazeApkBuildStep buildStep = runContext.getBuildStep();
try {
return buildStep.build(context, deviceSession);
} catch (Exception e) {
LOG.error(e);
return false;
}
});
}
private final class BlazeAndroidRunState implements RunProfileState {
private final Module module;
private final ExecutionEnvironment env;
private final String launchConfigName;
private final BlazeAndroidDeviceSelector.DeviceSession deviceSession;
private final BlazeAndroidRunContext runContext;
private final LaunchOptions launchOptions;
private BlazeAndroidRunState(Module module,
ExecutionEnvironment env,
String launchConfigName,
LaunchOptions launchOptions,
BlazeAndroidDeviceSelector.DeviceSession deviceSession,
BlazeAndroidRunContext runContext) {
this.module = module;
this.env = env;
this.launchConfigName = launchConfigName;
this.deviceSession = deviceSession;
this.runContext = runContext;
this.launchOptions = launchOptions;
}
@Nullable
@Override
public ExecutionResult execute(Executor executor, @NotNull ProgramRunner runner) throws ExecutionException {
ProcessHandler processHandler;
ConsoleView console;
ApplicationIdProvider applicationIdProvider = runContext.getApplicationIdProvider();
String applicationId;
try {
applicationId = applicationIdProvider.getPackageName();
}
catch (ApkProvisionException e) {
throw new ExecutionException("Unable to obtain application id", e);
}
LaunchTasksProvider launchTasksProvider = runContext.getLaunchTasksProvider(launchOptions, debuggerManager);
DeviceFutures deviceFutures = deviceSession.deviceFutures;
assert deviceFutures != null;
ProcessHandler previousSessionProcessHandler = deviceSession.sessionInfo != null ?
deviceSession.sessionInfo.getProcessHandler() : null;
if (launchTasksProvider.createsNewProcess()) {
// In the case of cold swap, there is an existing process that is connected, but we are going to launch a new one.
// Detach the previous process handler so that we don't end up with 2 run tabs for the same launch (the existing one
// and the new one).
if (previousSessionProcessHandler != null) {
previousSessionProcessHandler.detachProcess();
}
processHandler = new AndroidProcessHandler(applicationId, launchTasksProvider.monitorRemoteProcess());
console = runContext.getConsoleProvider().createAndAttach(module.getProject(), processHandler, executor);
} else {
assert previousSessionProcessHandler != null : "No process handler from previous session, yet current tasks don't create one";
processHandler = previousSessionProcessHandler;
console = null;
}
LaunchInfo launchInfo = new LaunchInfo(executor, runner, env, runContext.getConsoleProvider());
LaunchTaskRunner task = new LaunchTaskRunner(module.getProject(),
launchConfigName,
launchInfo,
processHandler,
deviceSession.deviceFutures,
launchTasksProvider);
ProgressManager.getInstance().run(task);
return console == null ? null : new DefaultExecutionResult(console, processHandler);
}
}
@Override
public void readExternal(Element element) throws InvalidDataException {
deployTargetManager.readExternal(element);
debuggerManager.readExternal(element);
}
@Override
public void writeExternal(Element element) throws WriteExternalException {
deployTargetManager.writeExternal(element);
debuggerManager.writeExternal(element);
}
}