Don't add all system env variables to the process (PY-21648)
[idea/community.git] / python / src / com / jetbrains / python / run / PythonCommandLineState.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.jetbrains.python.run;
17
18 import com.google.common.collect.Lists;
19 import com.google.common.collect.Maps;
20 import com.google.common.collect.Sets;
21 import com.intellij.execution.DefaultExecutionResult;
22 import com.intellij.execution.ExecutionException;
23 import com.intellij.execution.ExecutionResult;
24 import com.intellij.execution.Executor;
25 import com.intellij.execution.configurations.*;
26 import com.intellij.execution.configurations.GeneralCommandLine.ParentEnvironmentType;
27 import com.intellij.execution.filters.TextConsoleBuilder;
28 import com.intellij.execution.filters.TextConsoleBuilderFactory;
29 import com.intellij.execution.filters.UrlFilter;
30 import com.intellij.execution.process.ProcessHandler;
31 import com.intellij.execution.process.ProcessTerminatedListener;
32 import com.intellij.execution.runners.ExecutionEnvironment;
33 import com.intellij.execution.runners.ProgramRunner;
34 import com.intellij.execution.ui.ConsoleView;
35 import com.intellij.facet.Facet;
36 import com.intellij.facet.FacetManager;
37 import com.intellij.openapi.diagnostic.Logger;
38 import com.intellij.openapi.module.Module;
39 import com.intellij.openapi.module.ModuleManager;
40 import com.intellij.openapi.module.ModuleUtilCore;
41 import com.intellij.openapi.project.Project;
42 import com.intellij.openapi.projectRoots.Sdk;
43 import com.intellij.openapi.projectRoots.SdkAdditionalData;
44 import com.intellij.openapi.roots.*;
45 import com.intellij.openapi.roots.impl.libraries.LibraryImpl;
46 import com.intellij.openapi.roots.libraries.Library;
47 import com.intellij.openapi.roots.libraries.PersistentLibraryKind;
48 import com.intellij.openapi.util.io.FileUtil;
49 import com.intellij.openapi.util.text.StringUtil;
50 import com.intellij.openapi.vfs.JarFileSystem;
51 import com.intellij.openapi.vfs.VirtualFile;
52 import com.intellij.openapi.vfs.encoding.EncodingProjectManager;
53 import com.intellij.remote.RemoteProcessControl;
54 import com.intellij.util.PlatformUtils;
55 import com.jetbrains.python.PythonHelpersLocator;
56 import com.jetbrains.python.console.PyDebugConsoleBuilder;
57 import com.jetbrains.python.debugger.PyDebugRunner;
58 import com.jetbrains.python.debugger.PyDebuggerOptionsProvider;
59 import com.jetbrains.python.facet.LibraryContributingFacet;
60 import com.jetbrains.python.facet.PythonPathContributingFacet;
61 import com.jetbrains.python.library.PythonLibraryType;
62 import com.jetbrains.python.remote.PyRemotePathMapper;
63 import com.jetbrains.python.sdk.PySdkUtil;
64 import com.jetbrains.python.sdk.PythonEnvUtil;
65 import com.jetbrains.python.sdk.PythonSdkAdditionalData;
66 import com.jetbrains.python.sdk.PythonSdkType;
67 import com.jetbrains.python.sdk.flavors.JythonSdkFlavor;
68 import com.jetbrains.python.sdk.flavors.PythonSdkFlavor;
69 import org.jetbrains.annotations.NotNull;
70 import org.jetbrains.annotations.Nullable;
71
72 import java.io.IOException;
73 import java.net.ServerSocket;
74 import java.nio.charset.Charset;
75 import java.util.*;
76 import java.util.stream.Collectors;
77
78 /**
79  * @author traff, Leonid Shalupov
80  */
81 public abstract class PythonCommandLineState extends CommandLineState {
82   private static final Logger LOG = Logger.getInstance("#com.jetbrains.python.run.PythonCommandLineState");
83
84   // command line has a number of fixed groups of parameters; patchers should only operate on them and not the raw list.
85
86   public static final String GROUP_EXE_OPTIONS = "Exe Options";
87   public static final String GROUP_DEBUGGER = "Debugger";
88   public static final String GROUP_PROFILER = "Profiler";
89   public static final String GROUP_COVERAGE = "Coverage";
90   public static final String GROUP_SCRIPT = "Script";
91   private final AbstractPythonRunConfiguration myConfig;
92
93   private Boolean myMultiprocessDebug = null;
94   private boolean myRunWithPty = PtyCommandLine.isEnabled();
95
96   public boolean isDebug() {
97     return PyDebugRunner.PY_DEBUG_RUNNER.equals(getEnvironment().getRunner().getRunnerId());
98   }
99
100   public static ServerSocket createServerSocket() throws ExecutionException {
101     final ServerSocket serverSocket;
102     try {
103       //noinspection SocketOpenedButNotSafelyClosed
104       serverSocket = new ServerSocket(0);
105     }
106     catch (IOException e) {
107       throw new ExecutionException("Failed to find free socket port", e);
108     }
109     return serverSocket;
110   }
111
112   public PythonCommandLineState(AbstractPythonRunConfiguration runConfiguration, ExecutionEnvironment env) {
113     super(env);
114     myConfig = runConfiguration;
115   }
116
117   @Nullable
118   public PythonSdkFlavor getSdkFlavor() {
119     return PythonSdkFlavor.getFlavor(myConfig.getInterpreterPath());
120   }
121
122   public Sdk getSdk() {
123     return myConfig.getSdk();
124   }
125
126   @NotNull
127   @Override
128   public ExecutionResult execute(@NotNull Executor executor, @NotNull ProgramRunner runner) throws ExecutionException {
129     return execute(executor, (CommandLinePatcher[])null);
130   }
131
132   public ExecutionResult execute(Executor executor, CommandLinePatcher... patchers) throws ExecutionException {
133     final ProcessHandler processHandler = startProcess(patchers);
134     final ConsoleView console = createAndAttachConsole(myConfig.getProject(), processHandler, executor);
135     return new DefaultExecutionResult(console, processHandler, createActions(console, processHandler));
136   }
137
138   @NotNull
139   protected ConsoleView createAndAttachConsole(Project project, ProcessHandler processHandler, Executor executor)
140     throws ExecutionException {
141     final ConsoleView consoleView = createConsoleBuilder(project).getConsole();
142     consoleView.addMessageFilter(createUrlFilter(processHandler));
143
144     addTracebackFilter(project, consoleView, processHandler);
145
146     consoleView.attachToProcess(processHandler);
147     return consoleView;
148   }
149
150   protected void addTracebackFilter(Project project, ConsoleView consoleView, ProcessHandler processHandler) {
151     if (PySdkUtil.isRemote(myConfig.getSdk())) {
152       assert processHandler instanceof RemoteProcessControl;
153       consoleView
154         .addMessageFilter(new PyRemoteTracebackFilter(project, myConfig.getWorkingDirectory(), (RemoteProcessControl)processHandler));
155     }
156     else {
157       consoleView.addMessageFilter(new PythonTracebackFilter(project, myConfig.getWorkingDirectorySafe()));
158     }
159     consoleView.addMessageFilter(createUrlFilter(processHandler)); // Url filter is always nice to have
160   }
161
162   private TextConsoleBuilder createConsoleBuilder(Project project) {
163     if (isDebug()) {
164       return new PyDebugConsoleBuilder(project, PythonSdkType.findSdkByPath(myConfig.getInterpreterPath()));
165     }
166     else {
167       return TextConsoleBuilderFactory.getInstance().createBuilder(project);
168     }
169   }
170
171   @Override
172   @NotNull
173   protected ProcessHandler startProcess() throws ExecutionException {
174     return startProcess(new CommandLinePatcher[]{});
175   }
176
177   /**
178    * Patches the command line parameters applying patchers from first to last, and then runs it.
179    *
180    * @param patchers any number of patchers; any patcher may be null, and the whole argument may be null.
181    * @return handler of the started process
182    * @throws ExecutionException
183    */
184   protected ProcessHandler startProcess(CommandLinePatcher... patchers) throws ExecutionException {
185     GeneralCommandLine commandLine = generateCommandLine(patchers);
186
187     // Extend command line
188     PythonRunConfigurationExtensionsManager.getInstance()
189       .patchCommandLine(myConfig, getRunnerSettings(), commandLine, getEnvironment().getRunner().getRunnerId());
190     Sdk sdk = PythonSdkType.findSdkByPath(myConfig.getInterpreterPath());
191     final ProcessHandler processHandler;
192     if (PySdkUtil.isRemote(sdk)) {
193       PyRemotePathMapper pathMapper = createRemotePathMapper();
194       processHandler = createRemoteProcessStarter().startRemoteProcess(sdk, commandLine, myConfig.getProject(), pathMapper);
195     }
196     else {
197       EncodingEnvironmentUtil.setLocaleEnvironmentIfMac(commandLine);
198       processHandler = doCreateProcess(commandLine);
199       ProcessTerminatedListener.attach(processHandler);
200     }
201
202     // attach extensions
203     PythonRunConfigurationExtensionsManager.getInstance().attachExtensionsToProcess(myConfig, processHandler, getRunnerSettings());
204
205     return processHandler;
206   }
207
208   @Nullable
209   private PyRemotePathMapper createRemotePathMapper() {
210     if (myConfig.getMappingSettings() == null) {
211       return null;
212     }
213     else {
214       return PyRemotePathMapper.fromSettings(myConfig.getMappingSettings(), PyRemotePathMapper.PyPathMappingType.USER_DEFINED);
215     }
216   }
217
218   protected PyRemoteProcessStarter createRemoteProcessStarter() {
219     return new PyRemoteProcessStarter();
220   }
221
222
223   public GeneralCommandLine generateCommandLine(CommandLinePatcher[] patchers) {
224     GeneralCommandLine commandLine = generateCommandLine();
225     if (patchers != null) {
226       for (CommandLinePatcher patcher : patchers) {
227         if (patcher != null) patcher.patchCommandLine(commandLine);
228       }
229     }
230     return commandLine;
231   }
232
233   protected ProcessHandler doCreateProcess(GeneralCommandLine commandLine) throws ExecutionException {
234     return PythonProcessRunner.createProcess(commandLine);
235   }
236
237   public GeneralCommandLine generateCommandLine() {
238     GeneralCommandLine commandLine = createPythonCommandLine(myConfig.getProject(), myConfig, isDebug(), myRunWithPty);
239
240     buildCommandLineParameters(commandLine);
241
242     customizeEnvironmentVars(commandLine.getEnvironment(), myConfig.isPassParentEnvs());
243
244     return commandLine;
245   }
246
247   @NotNull
248   public static GeneralCommandLine createPythonCommandLine(Project project, PythonRunParams config, boolean isDebug, boolean runWithPty) {
249     GeneralCommandLine commandLine = generalCommandLine(runWithPty);
250
251     commandLine.withCharset(EncodingProjectManager.getInstance(project).getDefaultCharset());
252
253     createStandardGroups(commandLine);
254
255     initEnvironment(project, commandLine, config, isDebug);
256
257     setRunnerPath(project, commandLine, config);
258
259     return commandLine;
260   }
261
262   private static GeneralCommandLine generalCommandLine(boolean runWithPty) {
263     return runWithPty ? new PtyCommandLine() : new GeneralCommandLine();
264   }
265
266   /**
267    * Creates a number of parameter groups in the command line:
268    * GROUP_EXE_OPTIONS, GROUP_DEBUGGER, GROUP_SCRIPT.
269    * These are necessary for command line patchers to work properly.
270    *
271    * @param commandLine
272    */
273   public static void createStandardGroups(GeneralCommandLine commandLine) {
274     ParametersList params = commandLine.getParametersList();
275     params.addParamsGroup(GROUP_EXE_OPTIONS);
276     params.addParamsGroup(GROUP_DEBUGGER);
277     params.addParamsGroup(GROUP_PROFILER);
278     params.addParamsGroup(GROUP_COVERAGE);
279     params.addParamsGroup(GROUP_SCRIPT);
280   }
281
282   protected static void initEnvironment(Project project, GeneralCommandLine commandLine, PythonRunParams myConfig, boolean isDebug) {
283     Map<String, String> env = Maps.newHashMap();
284
285     setupEncodingEnvs(env, commandLine.getCharset());
286
287     if (myConfig.getEnvs() != null) {
288       env.putAll(myConfig.getEnvs());
289     }
290
291     addCommonEnvironmentVariables(getInterpreterPath(project, myConfig), env);
292
293     setupVirtualEnvVariables(myConfig, env, myConfig.getSdkHome());
294
295     commandLine.getEnvironment().clear();
296     commandLine.getEnvironment().putAll(env);
297     commandLine.withParentEnvironmentType(myConfig.isPassParentEnvs() ? ParentEnvironmentType.CONSOLE : ParentEnvironmentType.NONE);
298
299
300     buildPythonPath(project, commandLine, myConfig, isDebug);
301   }
302
303   private static void setupVirtualEnvVariables(PythonRunParams myConfig, Map<String, String> env, String sdkHome) {
304     if (PythonSdkType.isVirtualEnv(sdkHome)) {
305       PyVirtualEnvReader reader = new PyVirtualEnvReader(sdkHome);
306       if (reader.getActivate() != null) {
307         try {
308           env.putAll(reader.readShellEnv().entrySet().stream().filter((entry) -> PyVirtualEnvReader.Companion.getVirtualEnvVars().contains(entry.getKey())
309           ).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
310
311           for (Map.Entry<String, String> e : myConfig.getEnvs().entrySet()) {
312             if ("PATH".equals(e.getKey())) {
313               env.put(e.getKey(), PythonEnvUtil.appendToPathEnvVar(env.get("PATH"), e.getValue()));
314             }
315             else {
316               env.put(e.getKey(), e.getValue());
317             }
318           }
319         }
320         catch (Exception e) {
321           LOG.error("Couldn't read virtualenv variables", e);
322         }
323       }
324     }
325   }
326
327   protected static void addCommonEnvironmentVariables(@Nullable String homePath, Map<String, String> env) {
328     PythonEnvUtil.setPythonUnbuffered(env);
329     if (homePath != null) {
330       PythonEnvUtil.resetHomePathChanges(homePath, env);
331     }
332     env.put("PYCHARM_HOSTED", "1");
333   }
334
335   public void customizeEnvironmentVars(Map<String, String> envs, boolean passParentEnvs) {
336   }
337
338   private static void setupEncodingEnvs(Map<String, String> envs, Charset charset) {
339     PythonSdkFlavor.setupEncodingEnvs(envs, charset);
340   }
341
342   private static void buildPythonPath(Project project, GeneralCommandLine commandLine, PythonRunParams config, boolean isDebug) {
343     Sdk pythonSdk = PythonSdkType.findSdkByPath(config.getSdkHome());
344     if (pythonSdk != null) {
345       List<String> pathList = Lists.newArrayList(getAddedPaths(pythonSdk));
346       pathList.addAll(collectPythonPath(project, config, isDebug));
347       initPythonPath(commandLine, config.isPassParentEnvs(), pathList, config.getSdkHome());
348     }
349   }
350
351   public static void initPythonPath(GeneralCommandLine commandLine,
352                                     boolean passParentEnvs,
353                                     List<String> pathList,
354                                     final String interpreterPath) {
355     final PythonSdkFlavor flavor = PythonSdkFlavor.getFlavor(interpreterPath);
356     if (flavor != null) {
357       flavor.initPythonPath(commandLine, pathList);
358     }
359     else {
360       PythonSdkFlavor.initPythonPath(commandLine.getEnvironment(), passParentEnvs, pathList);
361     }
362   }
363
364   public static List<String> getAddedPaths(Sdk pythonSdk) {
365     List<String> pathList = new ArrayList<>();
366     final SdkAdditionalData sdkAdditionalData = pythonSdk.getSdkAdditionalData();
367     if (sdkAdditionalData instanceof PythonSdkAdditionalData) {
368       final Set<VirtualFile> addedPaths = ((PythonSdkAdditionalData)sdkAdditionalData).getAddedPathFiles();
369       for (VirtualFile file : addedPaths) {
370         addToPythonPath(file, pathList);
371       }
372     }
373     return pathList;
374   }
375
376   private static void addToPythonPath(VirtualFile file, Collection<String> pathList) {
377     if (file.getFileSystem() instanceof JarFileSystem) {
378       final VirtualFile realFile = JarFileSystem.getInstance().getVirtualFileForJar(file);
379       if (realFile != null) {
380         addIfNeeded(realFile, pathList);
381       }
382     }
383     else {
384       addIfNeeded(file, pathList);
385     }
386   }
387
388   private static void addIfNeeded(@NotNull final VirtualFile file, @NotNull final Collection<String> pathList) {
389     addIfNeeded(pathList, file.getPath());
390   }
391
392   protected static void addIfNeeded(Collection<String> pathList, String path) {
393     final Set<String> vals = Sets.newHashSet(pathList);
394     final String filePath = FileUtil.toSystemDependentName(path);
395     if (!vals.contains(filePath)) {
396       pathList.add(filePath);
397     }
398   }
399
400   protected static Collection<String> collectPythonPath(Project project, PythonRunParams config, boolean isDebug) {
401     final Module module = getModule(project, config);
402     final HashSet<String> pythonPath =
403       Sets.newHashSet(collectPythonPath(module, config.shouldAddContentRoots(), config.shouldAddSourceRoots()));
404
405     if (isDebug && PythonSdkFlavor.getFlavor(config.getSdkHome()) instanceof JythonSdkFlavor) {
406       //that fixes Jython problem changing sys.argv on execfile, see PY-8164
407       pythonPath.add(PythonHelpersLocator.getHelperPath("pycharm"));
408       pythonPath.add(PythonHelpersLocator.getHelperPath("pydev"));
409     }
410
411     return pythonPath;
412   }
413
414   @Nullable
415   private static Module getModule(Project project, PythonRunParams config) {
416     String name = config.getModuleName();
417     return StringUtil.isEmpty(name) ? null : ModuleManager.getInstance(project).findModuleByName(name);
418   }
419
420   @NotNull
421   public static Collection<String> collectPythonPath(@Nullable Module module) {
422     return collectPythonPath(module, true, true);
423   }
424
425   @NotNull
426   public static Collection<String> collectPythonPath(@Nullable Module module, boolean addContentRoots,
427                                                      boolean addSourceRoots) {
428     Collection<String> pythonPathList = Sets.newLinkedHashSet();
429     if (module != null) {
430       Set<Module> dependencies = new HashSet<>();
431       ModuleUtilCore.getDependencies(module, dependencies);
432
433       if (addContentRoots) {
434         addRoots(pythonPathList, ModuleRootManager.getInstance(module).getContentRoots());
435         for (Module dependency : dependencies) {
436           addRoots(pythonPathList, ModuleRootManager.getInstance(dependency).getContentRoots());
437         }
438       }
439       if (addSourceRoots) {
440         addRoots(pythonPathList, ModuleRootManager.getInstance(module).getSourceRoots());
441         for (Module dependency : dependencies) {
442           addRoots(pythonPathList, ModuleRootManager.getInstance(dependency).getSourceRoots());
443         }
444       }
445
446       addLibrariesFromModule(module, pythonPathList);
447       addRootsFromModule(module, pythonPathList);
448       for (Module dependency : dependencies) {
449         addLibrariesFromModule(dependency, pythonPathList);
450         addRootsFromModule(dependency, pythonPathList);
451       }
452     }
453     return pythonPathList;
454   }
455
456   private static void addLibrariesFromModule(Module module, Collection<String> list) {
457     final OrderEntry[] entries = ModuleRootManager.getInstance(module).getOrderEntries();
458     for (OrderEntry entry : entries) {
459       if (entry instanceof LibraryOrderEntry) {
460         final String name = ((LibraryOrderEntry)entry).getLibraryName();
461         if (name != null && name.endsWith(LibraryContributingFacet.PYTHON_FACET_LIBRARY_NAME_SUFFIX)) {
462           // skip libraries from Python facet
463           continue;
464         }
465         for (VirtualFile root : ((LibraryOrderEntry)entry).getRootFiles(OrderRootType.CLASSES)) {
466           final Library library = ((LibraryOrderEntry)entry).getLibrary();
467           if (!PlatformUtils.isPyCharm()) {
468             addToPythonPath(root, list);
469           }
470           else if (library instanceof LibraryImpl) {
471             final PersistentLibraryKind<?> kind = ((LibraryImpl)library).getKind();
472             if (kind == PythonLibraryType.getInstance().getKind()) {
473               addToPythonPath(root, list);
474             }
475           }
476         }
477       }
478     }
479   }
480
481   private static void addRootsFromModule(Module module, Collection<String> pythonPathList) {
482
483     // for Jython
484     final CompilerModuleExtension extension = CompilerModuleExtension.getInstance(module);
485     if (extension != null) {
486       final VirtualFile path = extension.getCompilerOutputPath();
487       if (path != null) {
488         pythonPathList.add(path.getPath());
489       }
490       final VirtualFile pathForTests = extension.getCompilerOutputPathForTests();
491       if (pathForTests != null) {
492         pythonPathList.add(pathForTests.getPath());
493       }
494     }
495
496     //additional paths from facets (f.e. buildout)
497     final Facet[] facets = FacetManager.getInstance(module).getAllFacets();
498     for (Facet facet : facets) {
499       if (facet instanceof PythonPathContributingFacet) {
500         List<String> more_paths = ((PythonPathContributingFacet)facet).getAdditionalPythonPath();
501         if (more_paths != null) pythonPathList.addAll(more_paths);
502       }
503     }
504   }
505
506   private static void addRoots(Collection<String> pythonPathList, VirtualFile[] roots) {
507     for (VirtualFile root : roots) {
508       addToPythonPath(root, pythonPathList);
509     }
510   }
511
512   protected static void setRunnerPath(Project project, GeneralCommandLine commandLine, PythonRunParams config) {
513     String interpreterPath = getInterpreterPath(project, config);
514     if (StringUtil.isNotEmpty(interpreterPath)) {
515       commandLine.setExePath(FileUtil.toSystemDependentName(interpreterPath));
516     }
517   }
518
519   @Nullable
520   public static String getInterpreterPath(Project project, PythonRunParams config) {
521     String sdkHome = config.getSdkHome();
522     if (config.isUseModuleSdk() || StringUtil.isEmpty(sdkHome)) {
523       Module module = getModule(project, config);
524
525       Sdk sdk = PythonSdkType.findPythonSdk(module);
526
527       if (sdk != null) {
528         sdkHome = sdk.getHomePath();
529       }
530     }
531
532     return sdkHome;
533   }
534
535   protected String getInterpreterPath() throws ExecutionException {
536     String interpreterPath = myConfig.getInterpreterPath();
537     if (interpreterPath == null) {
538       throw new ExecutionException("Cannot find Python interpreter for this run configuration");
539     }
540     return interpreterPath;
541   }
542
543   protected void buildCommandLineParameters(GeneralCommandLine commandLine) {
544   }
545
546   public boolean isMultiprocessDebug() {
547     if (myMultiprocessDebug != null) {
548       return myMultiprocessDebug;
549     }
550     else {
551       return PyDebuggerOptionsProvider.getInstance(myConfig.getProject()).isAttachToSubprocess();
552     }
553   }
554
555   public void setMultiprocessDebug(boolean multiprocessDebug) {
556     myMultiprocessDebug = multiprocessDebug;
557   }
558
559   public void setRunWithPty(boolean runWithPty) {
560     myRunWithPty = runWithPty;
561   }
562
563   @NotNull
564   protected UrlFilter createUrlFilter(ProcessHandler handler) {
565     return new UrlFilter();
566   }
567 }