PY-40486 Introduce Python interpreter introspection procedure implementation based on Targets API

GitOrigin-RevId: da24a7bef8e812d8f5fefeebed7852bf3bb726c5
This commit is contained in:
Alexander Koshevoy
2020-08-06 15:28:33 +03:00
committed by intellij-monorepo-bot
parent ec320a15a3
commit 180883b8e8
4 changed files with 272 additions and 145 deletions

View File

@@ -0,0 +1,168 @@
package com.jetbrains.python.sdk.skeletons;
import com.google.common.collect.ImmutableMap;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.process.CapturingProcessHandler;
import com.intellij.execution.process.ProcessOutput;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Time;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PythonHelpersLocator;
import com.jetbrains.python.sdk.InvalidSdkException;
import com.jetbrains.python.sdk.PySdkUtil;
import com.jetbrains.python.sdk.PythonEnvUtil;
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class PyLegacySkeletonGenerator extends PySkeletonGenerator {
/**
* @param skeletonPath path where skeletons should be generated
* @param pySdk SDK
* @param currentFolder current folder (some flavors may search for binary files there) or null if unknown
*/
public PyLegacySkeletonGenerator(String skeletonPath,
@NotNull Sdk pySdk,
@Nullable String currentFolder) {
super(skeletonPath, pySdk, currentFolder);
}
@Override
@NotNull
public final Builder commandBuilder() {
final Builder builder = new LegacyBuilder();
if (myCurrentFolder != null) {
builder.workingDir(myCurrentFolder);
}
return builder;
}
/**
* @param ensureSuccess throw {@link InvalidSdkException} containing additional diagnostic on process non-zero exit code.
* You might want to disable it for commands where non-zero exit code is possible for situations other
* than misconfigured interpreter or execution error in order to inspect the output manually.
*/
@NotNull
protected ProcessOutput runProcess(@NotNull Builder builder, boolean ensureSuccess) throws InvalidSdkException {
ProcessOutput output = builder.runProcess();
if (ensureSuccess && output.getExitCode() != 0) {
throw new InvalidSdkException(formatGeneratorFailureMessage(output));
}
return output;
}
// [targets-api] this should be left as-is because it deals with non-structured `commandLine`
protected @NotNull ProcessOutput getProcessOutput(@Nullable String homePath,
String @NotNull [] commandLine,
@Nullable String stdin,
@Nullable Map<String, String> extraEnv,
int timeout) throws InvalidSdkException {
final byte[] bytes = stdin != null ? stdin.getBytes(StandardCharsets.UTF_8) : null;
return PySdkUtil.getProcessOutput(homePath, commandLine, extraEnv, timeout, bytes, true);
}
@NotNull
protected ProcessOutput runProcessWithLineOutputListener(@NotNull String homePath,
@NotNull List<String> cmd,
@NotNull Map<String, String> env,
@Nullable String stdin,
int timeout,
@NotNull LineWiseProcessOutputListener listener)
throws ExecutionException, InvalidSdkException {
final GeneralCommandLine commandLine = new GeneralCommandLine(cmd)
.withWorkDirectory(homePath)
.withEnvironment(env);
final CapturingProcessHandler handler = new CapturingProcessHandler(commandLine);
if (stdin != null) {
sendLineToProcessInput(handler, stdin);
}
handler.addProcessListener(new LineWiseProcessOutputListener.Adapter(listener));
return handler.runProcess(timeout);
}
private final class LegacyBuilder extends Builder {
@NotNull
public List<String> getCommandLine() {
final List<String> commandLine = new ArrayList<>();
commandLine.add(mySdk.getHomePath());
commandLine.add(PythonHelpersLocator.getHelperPath(GENERATOR3));
commandLine.add("-d");
commandLine.add(mySkeletonsPath);
if (!ContainerUtil.isEmpty(myAssemblyRefs)) {
commandLine.add("-c");
commandLine.add(StringUtil.join(myAssemblyRefs, ";"));
}
if (!ContainerUtil.isEmpty(myExtraSysPath)) {
commandLine.add("-s");
commandLine.add(StringUtil.join(myExtraSysPath, File.pathSeparator));
}
commandLine.addAll(myExtraArgs);
if (StringUtil.isNotEmpty(myTargetModuleName)) {
commandLine.add(myTargetModuleName);
if (StringUtil.isNotEmpty(myTargetModulePath)) {
commandLine.add(myTargetModulePath);
}
}
return commandLine;
}
@NotNull
public Map<String, String> getEnvironment() {
Map<String, String> env = new HashMap<>();
final PythonSdkFlavor flavor = PythonSdkFlavor.getFlavor(mySdk);
final String flavorPathParam = flavor != null ? flavor.envPathParam() : null;
// TODO Investigate whether it's possible to pass this directory as an ordinary "extraSysPath" entry
if (myWorkingDir != null && flavorPathParam != null) {
env = PySdkUtil.mergeEnvVariables(env, ImmutableMap.of(flavorPathParam, myWorkingDir));
}
env = PySdkUtil.mergeEnvVariables(env, PySdkUtil.activateVirtualEnv(mySdk));
PythonEnvUtil.setPythonDontWriteBytecode(env);
if (myPrebuilt) {
env.put("IS_PREGENERATED_SKELETONS", "1");
}
return env;
}
@NotNull
public String getWorkingDir() throws InvalidSdkException {
if (myWorkingDir != null) {
return myWorkingDir;
}
final String binaryPath = mySdk.getHomePath();
if (binaryPath == null) throw new InvalidSdkException("Broken home path for " + mySdk.getName());
return new File(binaryPath).getParent();
}
@Override
@NotNull
public ProcessOutput runProcess() throws InvalidSdkException {
return getProcessOutput(getWorkingDir(),
ArrayUtil.toStringArray(getCommandLine()),
getStdin(),
getEnvironment(),
getTimeout(Time.MINUTE * 10));
}
@Override
public @NotNull ProcessOutput runProcessWithLineOutputListener(@NotNull LineWiseProcessOutputListener listener)
throws InvalidSdkException, ExecutionException {
return PyLegacySkeletonGenerator.this.runProcessWithLineOutputListener(getWorkingDir(),
getCommandLine(),
getEnvironment(),
myStdin,
getTimeout(Time.MINUTE * 20),
listener);
}
}
}

View File

@@ -1,13 +1,10 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.sdk.skeletons;
import com.google.common.collect.ImmutableMap;
import com.google.gson.*;
import com.google.gson.annotations.SerializedName;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.process.BaseProcessHandler;
import com.intellij.execution.process.CapturingProcessHandler;
import com.intellij.execution.process.ProcessOutput;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
@@ -16,22 +13,17 @@ import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Time;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PySdkBundle;
import com.jetbrains.python.PythonHelpersLocator;
import com.jetbrains.python.sdk.InvalidSdkException;
import com.jetbrains.python.sdk.PySdkUtil;
import com.jetbrains.python.sdk.PythonEnvUtil;
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* This class serves two purposes. First, it's a wrapper around "generator3" helper script
@@ -83,7 +75,7 @@ import java.util.*;
*
* @see Builder
*/
public class PySkeletonGenerator {
public abstract class PySkeletonGenerator {
private static class Run {
static final Logger LOG = Logger.getInstance(Run.class);
}
@@ -97,8 +89,8 @@ public class PySkeletonGenerator {
private static final Gson ourGson = new GsonBuilder().create();
protected final Sdk mySdk;
@Nullable private final String myCurrentFolder;
private final String mySkeletonsPath;
@Nullable protected final String myCurrentFolder;
protected final String mySkeletonsPath;
/**
* @param skeletonPath path where skeletons should be generated
@@ -113,13 +105,7 @@ public class PySkeletonGenerator {
}
@NotNull
public final Builder commandBuilder() {
final Builder builder = new Builder();
if (myCurrentFolder != null) {
builder.workingDir(myCurrentFolder);
}
return builder;
}
public abstract Builder commandBuilder();
/**
@@ -127,18 +113,18 @@ public class PySkeletonGenerator {
* allowing to additionally customize how it's going to be launched and performing the
* default initialization before the run.
*/
public final class Builder {
private final List<String> myExtraSysPath = new ArrayList<>();
private final List<String> myAssemblyRefs = new ArrayList<>();
private final List<String> myExtraArgs = new ArrayList<>();
private String myWorkingDir;
private String myTargetModuleName;
private String myTargetModulePath;
private boolean myPrebuilt = false;
private int myTimeout;
private String myStdin;
public abstract class Builder {
protected final List<String> myExtraSysPath = new ArrayList<>();
protected final List<String> myAssemblyRefs = new ArrayList<>();
protected final List<String> myExtraArgs = new ArrayList<>();
protected String myWorkingDir;
protected String myTargetModuleName;
protected String myTargetModulePath;
protected boolean myPrebuilt = false;
protected int myTimeout;
protected String myStdin;
private Builder() {
protected Builder() {
}
@NotNull
@@ -200,78 +186,20 @@ public class PySkeletonGenerator {
return myStdin;
}
@NotNull
public List<String> getCommandLine() {
final List<String> commandLine = new ArrayList<>();
commandLine.add(mySdk.getHomePath());
commandLine.add(PythonHelpersLocator.getHelperPath(GENERATOR3));
commandLine.add("-d");
commandLine.add(mySkeletonsPath);
if (!ContainerUtil.isEmpty(myAssemblyRefs)) {
commandLine.add("-c");
commandLine.add(StringUtil.join(myAssemblyRefs, ";"));
}
if (!ContainerUtil.isEmpty(myExtraSysPath)) {
commandLine.add("-s");
commandLine.add(StringUtil.join(myExtraSysPath, File.pathSeparator));
}
commandLine.addAll(myExtraArgs);
if (StringUtil.isNotEmpty(myTargetModuleName)) {
commandLine.add(myTargetModuleName);
if (StringUtil.isNotEmpty(myTargetModulePath)) {
commandLine.add(myTargetModulePath);
}
}
return commandLine;
}
@NotNull
public Map<String, String> getEnvironment() {
Map<String, String> env = new HashMap<>();
final PythonSdkFlavor flavor = PythonSdkFlavor.getFlavor(mySdk);
final String flavorPathParam = flavor != null ? flavor.envPathParam() : null;
// TODO Investigate whether it's possible to pass this directory as an ordinary "extraSysPath" entry
if (myWorkingDir != null && flavorPathParam != null) {
env = PySdkUtil.mergeEnvVariables(env, ImmutableMap.of(flavorPathParam, myWorkingDir));
}
env = PySdkUtil.mergeEnvVariables(env, PySdkUtil.activateVirtualEnv(mySdk));
PythonEnvUtil.setPythonDontWriteBytecode(env);
if (myPrebuilt) {
env.put("IS_PREGENERATED_SKELETONS", "1");
}
return env;
}
@NotNull
public String getWorkingDir() throws InvalidSdkException {
if (myWorkingDir != null) {
return myWorkingDir;
}
final String binaryPath = mySdk.getHomePath();
if (binaryPath == null) {
throw new InvalidSdkException(PySdkBundle.message("python.skeleton.generator.broken.home.path", mySdk.getName()));
}
return new File(binaryPath).getParent();
}
public int getTimeout(int defaultTimeout) {
return myTimeout > 0 ? myTimeout : defaultTimeout;
}
/**
* @param ensureSuccess throw {@link InvalidSdkException} containing additional diagnostic on process non-zero exit code.
* You might want to disable it for commands where non-zero exit code is possible for situations other
* than misconfigured interpreter or execution error in order to inspect the output manually.
*/
@NotNull
public ProcessOutput runProcess(boolean ensureSuccess) throws InvalidSdkException {
return PySkeletonGenerator.this.runProcess(this, ensureSuccess);
}
public abstract ProcessOutput runProcess() throws InvalidSdkException;
@NotNull
public List<GenerationResult> runGeneration(@Nullable ProgressIndicator indicator) throws InvalidSdkException, ExecutionException {
return PySkeletonGenerator.this.runGeneration(this, indicator);
}
public abstract @NotNull ProcessOutput runProcessWithLineOutputListener(@NotNull LineWiseProcessOutputListener listener)
throws InvalidSdkException, ExecutionException;
}
@NotNull
@@ -337,31 +265,13 @@ public class PySkeletonGenerator {
}
};
final ProcessOutput output = runProcessWithLineOutputListener(builder.getWorkingDir(),
builder.getCommandLine(),
builder.getEnvironment(),
builder.myStdin,
builder.getTimeout(Time.MINUTE * 20),
listener);
final ProcessOutput output = builder.runProcessWithLineOutputListener(listener);
if (output.getExitCode() != 0) {
throw new InvalidSdkException(formatGeneratorFailureMessage(output));
}
return results;
}
@NotNull
protected ProcessOutput runProcess(@NotNull Builder builder, boolean ensureSuccess) throws InvalidSdkException {
final ProcessOutput output = getProcessOutput(builder.getWorkingDir(),
ArrayUtil.toStringArray(builder.getCommandLine()),
builder.getStdin(),
builder.getEnvironment(),
builder.getTimeout(Time.MINUTE * 10));
if (ensureSuccess && output.getExitCode() != 0) {
throw new InvalidSdkException(formatGeneratorFailureMessage(output));
}
return output;
}
public void finishSkeletonsGeneration() {
}
@@ -369,25 +279,6 @@ public class PySkeletonGenerator {
return new File(name).exists();
}
@NotNull
protected ProcessOutput runProcessWithLineOutputListener(@NotNull String homePath,
@NotNull List<String> cmd,
@NotNull Map<String, String> env,
@Nullable String stdin,
int timeout,
@NotNull LineWiseProcessOutputListener listener)
throws ExecutionException, InvalidSdkException {
final GeneralCommandLine commandLine = new GeneralCommandLine(cmd)
.withWorkDirectory(homePath)
.withEnvironment(env);
final CapturingProcessHandler handler = new CapturingProcessHandler(commandLine);
if (stdin != null) {
sendLineToProcessInput(handler, stdin);
}
handler.addProcessListener(new LineWiseProcessOutputListener.Adapter(listener));
return handler.runProcess(timeout);
}
public String getSkeletonsPath() {
return mySkeletonsPath;
}
@@ -395,17 +286,8 @@ public class PySkeletonGenerator {
public void prepare() {
}
protected @NotNull ProcessOutput getProcessOutput(@Nullable String homePath,
String @NotNull [] commandLine,
@Nullable String stdin,
@Nullable Map<String, String> extraEnv,
int timeout) throws InvalidSdkException {
final byte[] bytes = stdin != null ? stdin.getBytes(StandardCharsets.UTF_8) : null;
return PySdkUtil.getProcessOutput(homePath, commandLine, extraEnv, timeout, bytes, true);
}
@NotNull
private @NlsSafe String formatGeneratorFailureMessage(@NotNull ProcessOutput process) {
protected final @NlsSafe String formatGeneratorFailureMessage(@NotNull ProcessOutput process) {
final StringBuilder sb = new StringBuilder("failed to run ").append(GENERATOR3).append(" for ").append(mySdk.getHomePath());
if (process.isTimeout()) {
sb.append(": timed out.");