blob: 08f2a22efcdb8b377d644b18c19e2e52c12682b2 [file] [log] [blame]
/*
* Copyright 2017 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.python.run;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.idea.blaze.base.async.executor.BlazeExecutor;
import com.google.idea.blaze.base.async.process.ExternalTask;
import com.google.idea.blaze.base.command.BlazeCommand;
import com.google.idea.blaze.base.command.BlazeCommandName;
import com.google.idea.blaze.base.command.BlazeFlags;
import com.google.idea.blaze.base.command.buildresult.BuildResultHelper;
import com.google.idea.blaze.base.io.FileAttributeProvider;
import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
import com.google.idea.blaze.base.model.BlazeProjectData;
import com.google.idea.blaze.base.model.primitives.Label;
import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
import com.google.idea.blaze.base.projectview.ProjectViewManager;
import com.google.idea.blaze.base.projectview.ProjectViewSet;
import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
import com.google.idea.blaze.base.run.WithBrowserHyperlinkExecutionException;
import com.google.idea.blaze.base.run.confighandler.BlazeCommandGenericRunConfigurationRunner.BlazeCommandRunProfileState;
import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationRunner;
import com.google.idea.blaze.base.run.filter.BlazeTargetFilter;
import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
import com.google.idea.blaze.base.scope.BlazeContext;
import com.google.idea.blaze.base.scope.ScopedTask;
import com.google.idea.blaze.base.scope.output.StatusOutput;
import com.google.idea.blaze.base.scope.scopes.BlazeConsoleScope;
import com.google.idea.blaze.base.scope.scopes.IssuesScope;
import com.google.idea.blaze.base.settings.Blaze;
import com.google.idea.blaze.base.settings.BlazeUserSettings;
import com.google.idea.blaze.base.sync.data.BlazeDataStorage;
import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
import com.google.idea.blaze.base.util.SaveUtil;
import com.google.idea.blaze.python.run.filter.BlazePyFilterProvider;
import com.google.idea.common.experiments.BoolExperiment;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.ExecutionResult;
import com.intellij.execution.Executor;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.configurations.RunProfile;
import com.intellij.execution.configurations.RunProfileState;
import com.intellij.execution.configurations.WrappingRunConfiguration;
import com.intellij.execution.executors.DefaultDebugExecutor;
import com.intellij.execution.filters.Filter;
import com.intellij.execution.filters.TextConsoleBuilder;
import com.intellij.execution.process.KillableProcessHandler;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.execution.runners.ExecutionUtil;
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.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.util.PathUtil;
import com.intellij.util.execution.ParametersListUtil;
import com.jetbrains.python.console.PyDebugConsoleBuilder;
import com.jetbrains.python.console.PythonDebugLanguageConsoleView;
import com.jetbrains.python.run.PythonConfigurationType;
import com.jetbrains.python.run.PythonRunConfiguration;
import com.jetbrains.python.run.PythonScriptCommandLineState;
import java.io.File;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
/** Python-specific run configuration runner. */
public class BlazePyRunConfigurationRunner implements BlazeCommandRunConfigurationRunner {
/** Used to store a runner to an {@link ExecutionEnvironment}. */
private static final Key<AtomicReference<File>> EXECUTABLE_KEY =
Key.create("blaze.debug.py.executable");
private static final Logger logger = Logger.getInstance(BlazePyRunConfigurationRunner.class);
// Filter executables instead of files in the bin directory
// This bin directory isn't the right one, because we don't know the blaze binary
// or the config flags used to execute the build command
// Introduced March 2017
private static final BoolExperiment filterExecutableFiles =
new BoolExperiment("filter.executable.files", true);
/** Converts to the native python plugin debug configuration state */
static class BlazePyDummyRunProfileState implements RunProfileState {
final BlazeCommandRunConfiguration configuration;
BlazePyDummyRunProfileState(BlazeCommandRunConfiguration configuration) {
this.configuration = configuration;
}
PythonScriptCommandLineState toNativeState(ExecutionEnvironment env) throws ExecutionException {
File executable = env.getCopyableUserData(EXECUTABLE_KEY).get();
if (executable == null || StringUtil.isEmptyOrSpaces(executable.getPath())) {
throw new ExecutionException("No blaze output script found");
}
PythonRunConfiguration nativeConfig =
(PythonRunConfiguration)
PythonConfigurationType.getInstance()
.getFactory()
.createTemplateConfiguration(env.getProject());
nativeConfig.setScriptName(executable.getPath());
nativeConfig.setAddContentRoots(false);
nativeConfig.setAddSourceRoots(false);
nativeConfig.setWorkingDirectory(
Strings.nullToEmpty(
getRunfilesPath(executable, WorkspaceRoot.fromProjectSafe(env.getProject()))));
Module workspaceModule =
nativeConfig.getConfigurationModule().findModule(BlazeDataStorage.WORKSPACE_MODULE_NAME);
if (workspaceModule != null) {
nativeConfig.setModule(workspaceModule);
nativeConfig.setUseModuleSdk(true);
} else {
throw new ExecutionException(
"Can't find the workspace module when debugging a python target");
}
BlazeCommandRunConfigurationCommonState handlerState =
configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
if (handlerState != null) {
nativeConfig.setScriptParameters(Strings.emptyToNull(getScriptParams(handlerState)));
}
return new PythonScriptCommandLineState(nativeConfig, env) {
@Override
public boolean isDebug() {
return true;
}
@Override
protected ConsoleView createAndAttachConsole(
Project project, ProcessHandler processHandler, Executor executor)
throws ExecutionException {
ConsoleView consoleView = createConsoleBuilder(project, getSdk()).getConsole();
consoleView.addMessageFilter(createUrlFilter(processHandler));
addTracebackFilter(project, consoleView, processHandler);
consoleView.attachToProcess(processHandler);
return consoleView;
}
@Override
protected ProcessHandler doCreateProcess(GeneralCommandLine commandLine)
throws ExecutionException {
ProcessHandler handler = super.doCreateProcess(commandLine);
if (handler instanceof KillableProcessHandler) {
// SIGINT can cause the JVM to crash, when stopped at a breakpoint (IDEA-167432).
((KillableProcessHandler) handler).setShouldKillProcessSoftly(false);
}
return handler;
}
};
}
@Nullable
@Override
public ExecutionResult execute(Executor executor, ProgramRunner runner)
throws ExecutionException {
return null;
}
private static TextConsoleBuilder createConsoleBuilder(Project project, Sdk sdk) {
return new PyDebugConsoleBuilder(project, sdk) {
@Override
protected ConsoleView createConsole() {
PythonDebugLanguageConsoleView consoleView =
new PythonDebugLanguageConsoleView(project, sdk);
for (Filter filter : getFilters(project)) {
consoleView.addMessageFilter(filter);
}
return consoleView;
}
};
}
private static String getScriptParams(BlazeCommandRunConfigurationCommonState state) {
List<String> params = Lists.newArrayList(state.getExeFlagsState().getExpandedFlags());
String filterFlag = state.getTestFilterFlag();
if (filterFlag != null) {
params.add(filterFlag.substring((BlazeFlags.TEST_FILTER + "=").length()));
}
return ParametersListUtil.join(params);
}
}
private static ImmutableList<Filter> getFilters(Project project) {
return ImmutableList.<Filter>builder()
.addAll(BlazePyFilterProvider.getPyFilters(project))
.add(new BlazeTargetFilter(project))
.build();
}
@Override
public RunProfileState getRunProfileState(Executor executor, ExecutionEnvironment environment)
throws ExecutionException {
BlazeCommandRunConfiguration configuration = getConfiguration(environment);
if (isDebugging(environment)) {
environment.putCopyableUserData(EXECUTABLE_KEY, new AtomicReference<>());
return new BlazePyDummyRunProfileState(configuration);
}
return new BlazeCommandRunProfileState(environment, getFilters(environment.getProject()));
}
@Override
public boolean executeBeforeRunTask(ExecutionEnvironment env) {
if (!isDebugging(env)) {
return true;
}
try {
File executable = getExecutableToDebug(env);
env.getCopyableUserData(EXECUTABLE_KEY).set(executable);
if (executable != null) {
return true;
}
} catch (ExecutionException e) {
ExecutionUtil.handleExecutionError(
env.getProject(), env.getExecutor().getToolWindowId(), env.getRunProfile(), e);
logger.info(e);
}
return false;
}
private static boolean isDebugging(ExecutionEnvironment environment) {
Executor executor = environment.getExecutor();
return executor instanceof DefaultDebugExecutor;
}
private static BlazeCommandRunConfiguration getConfiguration(ExecutionEnvironment environment) {
RunProfile runProfile = environment.getRunProfile();
if (runProfile instanceof WrappingRunConfiguration) {
runProfile = ((WrappingRunConfiguration) runProfile).getPeer();
}
return (BlazeCommandRunConfiguration) runProfile;
}
/** Make a best-effort attempt to get the runfiles path. Returns null if it can't be found. */
@Nullable
private static String getRunfilesPath(File executable, @Nullable WorkspaceRoot root) {
if (root == null) {
return null;
}
String workspaceName = root.directory().getName();
File expectedPath = new File(executable.getPath() + ".runfiles", workspaceName);
if (FileAttributeProvider.getInstance().exists(expectedPath)) {
return expectedPath.getPath();
}
return null;
}
/**
* Builds blaze python target and returns the output build artifact.
*
* @throws ExecutionException if the target cannot be debugged.
*/
private static File getExecutableToDebug(ExecutionEnvironment env) throws ExecutionException {
BlazeCommandRunConfiguration configuration = getConfiguration(env);
final Project project = configuration.getProject();
BlazeProjectData blazeProjectData =
BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
if (blazeProjectData == null) {
throw new ExecutionException("Not synced yet, please sync project");
}
String validationError =
BlazePyDebugHelper.validateDebugTarget(env.getProject(), configuration.getTarget());
if (validationError != null) {
throw new WithBrowserHyperlinkExecutionException(validationError);
}
final BlazeCommandRunConfigurationCommonState handlerState =
(BlazeCommandRunConfigurationCommonState) configuration.getHandler().getState();
final WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
final ProjectViewSet projectViewSet =
ProjectViewManager.getInstance(project).getProjectViewSet();
BuildResultHelper buildResultHelper = BuildResultHelper.forFiles(file -> true);
boolean suppressConsole = BlazeUserSettings.getInstance().getSuppressConsoleForRunAction();
final ListenableFuture<Void> buildOperation =
BlazeExecutor.submitTask(
project,
new ScopedTask() {
@Override
protected void execute(BlazeContext context) {
context
.push(new IssuesScope(project))
.push(
new BlazeConsoleScope.Builder(project)
.setSuppressConsole(suppressConsole)
.build());
context.output(new StatusOutput("Building debug binary"));
BlazeCommand.Builder command =
BlazeCommand.builder(
Blaze.getBuildSystemProvider(project).getBinaryPath(),
BlazeCommandName.BUILD)
.addTargets(configuration.getTarget())
.addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
.addBlazeFlags(handlerState.getBlazeFlagsState().getExpandedFlags())
.addBlazeFlags(BlazePyDebugHelper.getAllBlazeDebugFlags())
.addBlazeFlags(buildResultHelper.getBuildFlags());
ExternalTask.builder(workspaceRoot)
.addBlazeCommand(command.build())
.context(context)
.stderr(
buildResultHelper.stderr(
new IssueOutputLineProcessor(project, context, workspaceRoot)))
.build()
.run();
}
});
try {
SaveUtil.saveAllFiles();
buildOperation.get();
} catch (InterruptedException | java.util.concurrent.ExecutionException e) {
throw new ExecutionException(e);
}
List<File> candidateFiles =
buildResultHelper
.getBuildArtifacts()
.stream()
.filter(fileFilter(blazeProjectData))
.collect(Collectors.toList());
if (candidateFiles.isEmpty()) {
throw new ExecutionException(
String.format("No output artifacts found when building %s", configuration.getTarget()));
}
File file = findExecutable((Label) configuration.getTarget(), candidateFiles);
if (file == null) {
throw new ExecutionException(
String.format(
"More than 1 executable was produced when building %s; don't know which one to debug",
configuration.getTarget()));
}
LocalFileSystem.getInstance().refreshIoFiles(ImmutableList.of(file));
return file;
}
private static Predicate<File> fileFilter(BlazeProjectData blazeProjectData) {
return filterExecutableFiles.getValue()
? File::canExecute
: f -> FileUtil.isAncestor(blazeProjectData.blazeInfo.getBlazeBinDirectory(), f, true);
}
/**
* Basic heuristic for choosing between multiple output files. Currently just looks for a filename
* matching the target name.
*/
@VisibleForTesting
@Nullable
static File findExecutable(Label target, List<File> outputs) {
if (outputs.size() == 1) {
return outputs.get(0);
}
String name = PathUtil.getFileName(target.targetName().toString());
for (File file : outputs) {
if (file.getName().equals(name)) {
return file;
}
}
return null;
}
}