| /* |
| * 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.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(); |
| 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 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); |
| } |
| } |
| } |