blob: 66fe3f083de830e4c5ffe6f6dc105cc034922437 [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 static org.jetbrains.android.actions.RunAndroidAvdManagerAction.getName;
import com.android.ddmlib.IDevice;
import com.android.tools.idea.run.AndroidProcessHandler;
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.DeviceFutures;
import com.android.tools.idea.run.LaunchInfo;
import com.android.tools.idea.run.LaunchOptions;
import com.android.tools.idea.run.LaunchTaskRunner;
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.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.idea.blaze.base.experiments.ExperimentScope;
import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationRunner;
import com.google.idea.blaze.base.run.smrunner.SmRunnerUtils;
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.IdeaLogScope;
import com.google.idea.blaze.base.scope.scopes.IssuesScope;
import com.google.idea.blaze.base.settings.BlazeUserSettings;
import com.google.idea.blaze.base.settings.BlazeUserSettings.BlazeConsolePopupBehavior;
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.Key;
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;
/**
* Supports the execution. Used by both android_binary and android_test.
*
* <p>Builds the APK and installs it, launches and debug tasks, etc.
*
* <p>Any indirection between android_binary/android_test, mobile-install, InstantRun etc. should
* come via the strategy class.
*/
public final class BlazeAndroidRunConfigurationRunner
implements BlazeCommandRunConfigurationRunner {
private static final Logger LOG = Logger.getInstance(BlazeAndroidRunConfigurationRunner.class);
private 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");
private final Module module;
private final BlazeAndroidRunContext runContext;
private final BlazeAndroidRunConfigurationDeployTargetManager deployTargetManager;
private final BlazeAndroidRunConfigurationDebuggerManager debuggerManager;
private final int runConfigId;
public BlazeAndroidRunConfigurationRunner(
Module module,
BlazeAndroidRunContext runContext,
BlazeAndroidRunConfigurationDeployTargetManager deployTargetManager,
BlazeAndroidRunConfigurationDebuggerManager debuggerManager,
int runConfigId) {
this.module = module;
this.runContext = runContext;
this.deployTargetManager = deployTargetManager;
this.debuggerManager = debuggerManager;
this.runConfigId = runConfigId;
}
@Override
@Nullable
public final RunProfileState getRunProfileState(final Executor executor, ExecutionEnvironment env)
throws ExecutionException {
final AndroidFacet facet = AndroidFacet.getInstance(module);
assert facet != null : "Enforced by fatal validation check in createRunner.";
final Project project = env.getProject();
runContext.augmentEnvironment(env);
boolean isDebug = executor instanceof DefaultDebugExecutor;
if (isDebug && !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, isDebug, 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 (isDebug) {
String error = canDebug(deviceFutures, facet, module.getName());
if (error != null) {
throw new ExecutionException(error);
}
}
LaunchOptions.Builder launchOptionsBuilder = getDefaultLaunchOptions().setDebug(isDebug);
runContext.augmentLaunchOptions(launchOptionsBuilder);
// 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(), launchOptionsBuilder, isDebug, 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);
}
@Override
public boolean executeBeforeRunTask(ExecutionEnvironment env) {
final Project project = env.getProject();
BlazeConsolePopupBehavior consolePopupBehavior =
BlazeUserSettings.getInstance().getSuppressConsoleForRunAction()
? BlazeConsolePopupBehavior.NEVER
: BlazeConsolePopupBehavior.ALWAYS;
return Scope.root(
context -> {
context
.push(new IssuesScope(project))
.push(new ExperimentScope())
.push(
new BlazeConsoleScope.Builder(project)
.setPopupBehavior(consolePopupBehavior)
.build())
.push(new IdeaLogScope());
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.Builder launchOptionsBuilder;
private final boolean isDebug;
private BlazeAndroidRunState(
Module module,
ExecutionEnvironment env,
String launchConfigName,
LaunchOptions.Builder launchOptionsBuilder,
boolean isDebug,
BlazeAndroidDeviceSelector.DeviceSession deviceSession,
BlazeAndroidRunContext runContext) {
this.module = module;
this.env = env;
this.launchConfigName = launchConfigName;
this.deviceSession = deviceSession;
this.runContext = runContext;
this.launchOptionsBuilder = launchOptionsBuilder;
this.isDebug = isDebug;
}
@Nullable
@Override
public ExecutionResult execute(Executor executor, ProgramRunner runner)
throws ExecutionException {
DefaultExecutionResult result = executeInner(executor, runner);
if (result == null) {
return null;
}
return SmRunnerUtils.attachRerunFailedTestsAction(result);
}
@Nullable
private DefaultExecutionResult executeInner(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(launchOptionsBuilder, isDebug, 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);
}
}
}