[git] rename field for consistency
[idea/community.git] / python / src / com / jetbrains / python / run / PythonCommandLineState.java
1 /*
2  * Copyright 2000-2015 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.Sets;
20 import com.intellij.execution.DefaultExecutionResult;
21 import com.intellij.execution.ExecutionException;
22 import com.intellij.execution.ExecutionResult;
23 import com.intellij.execution.Executor;
24 import com.intellij.execution.configurations.*;
25 import com.intellij.execution.configurations.GeneralCommandLine.ParentEnvironmentType;
26 import com.intellij.execution.filters.TextConsoleBuilder;
27 import com.intellij.execution.filters.TextConsoleBuilderFactory;
28 import com.intellij.execution.filters.UrlFilter;
29 import com.intellij.execution.process.ProcessHandler;
30 import com.intellij.execution.process.ProcessTerminatedListener;
31 import com.intellij.execution.runners.ExecutionEnvironment;
32 import com.intellij.execution.runners.ProgramRunner;
33 import com.intellij.execution.ui.ConsoleView;
34 import com.intellij.facet.Facet;
35 import com.intellij.facet.FacetManager;
36 import com.intellij.openapi.actionSystem.AnAction;
37 import com.intellij.openapi.module.Module;
38 import com.intellij.openapi.module.ModuleUtilCore;
39 import com.intellij.openapi.project.Project;
40 import com.intellij.openapi.projectRoots.Sdk;
41 import com.intellij.openapi.projectRoots.SdkAdditionalData;
42 import com.intellij.openapi.roots.*;
43 import com.intellij.openapi.roots.impl.libraries.LibraryImpl;
44 import com.intellij.openapi.roots.libraries.Library;
45 import com.intellij.openapi.roots.libraries.PersistentLibraryKind;
46 import com.intellij.openapi.util.io.FileUtil;
47 import com.intellij.openapi.vfs.JarFileSystem;
48 import com.intellij.openapi.vfs.VirtualFile;
49 import com.intellij.openapi.vfs.encoding.EncodingProjectManager;
50 import com.intellij.remote.RemoteProcessHandlerBase;
51 import com.intellij.util.PlatformUtils;
52 import com.intellij.util.containers.HashMap;
53 import com.jetbrains.python.PythonHelpersLocator;
54 import com.jetbrains.python.console.PyDebugConsoleBuilder;
55 import com.jetbrains.python.debugger.PyDebugRunner;
56 import com.jetbrains.python.debugger.PyDebuggerOptionsProvider;
57 import com.jetbrains.python.facet.LibraryContributingFacet;
58 import com.jetbrains.python.facet.PythonPathContributingFacet;
59 import com.jetbrains.python.library.PythonLibraryType;
60 import com.jetbrains.python.remote.PyRemotePathMapper;
61 import com.jetbrains.python.sdk.PySdkUtil;
62 import com.jetbrains.python.sdk.PythonEnvUtil;
63 import com.jetbrains.python.sdk.PythonSdkAdditionalData;
64 import com.jetbrains.python.sdk.PythonSdkType;
65 import com.jetbrains.python.sdk.flavors.JythonSdkFlavor;
66 import com.jetbrains.python.sdk.flavors.PythonSdkFlavor;
67 import org.jetbrains.annotations.NotNull;
68 import org.jetbrains.annotations.Nullable;
69
70 import java.io.IOException;
71 import java.net.ServerSocket;
72 import java.util.*;
73
74 /**
75  * @author Leonid Shalupov
76  */
77 public abstract class PythonCommandLineState extends CommandLineState {
78
79   // command line has a number of fixed groups of parameters; patchers should only operate on them and not the raw list.
80
81   public static final String GROUP_EXE_OPTIONS = "Exe Options";
82   public static final String GROUP_DEBUGGER = "Debugger";
83   public static final String GROUP_PROFILER = "Profiler";
84   public static final String GROUP_SCRIPT = "Script";
85   private final AbstractPythonRunConfiguration myConfig;
86
87   private Boolean myMultiprocessDebug = null;
88
89   public boolean isDebug() {
90     return PyDebugRunner.PY_DEBUG_RUNNER.equals(getEnvironment().getRunner().getRunnerId());
91   }
92
93   public static ServerSocket createServerSocket() throws ExecutionException {
94     final ServerSocket serverSocket;
95     try {
96       //noinspection SocketOpenedButNotSafelyClosed
97       serverSocket = new ServerSocket(0);
98     }
99     catch (IOException e) {
100       throw new ExecutionException("Failed to find free socket port", e);
101     }
102     return serverSocket;
103   }
104
105   public PythonCommandLineState(AbstractPythonRunConfiguration runConfiguration, ExecutionEnvironment env) {
106     super(env);
107     myConfig = runConfiguration;
108   }
109
110   @Nullable
111   public PythonSdkFlavor getSdkFlavor() {
112     return PythonSdkFlavor.getFlavor(myConfig.getInterpreterPath());
113   }
114
115   public Sdk getSdk() {
116     return myConfig.getSdk();
117   }
118
119   @NotNull
120   @Override
121   public ExecutionResult execute(@NotNull Executor executor, @NotNull ProgramRunner runner) throws ExecutionException {
122     return execute(executor, (CommandLinePatcher[])null);
123   }
124
125   public ExecutionResult execute(Executor executor, CommandLinePatcher... patchers) throws ExecutionException {
126     final ProcessHandler processHandler = startProcess(patchers);
127     final ConsoleView console = createAndAttachConsole(myConfig.getProject(), processHandler, executor);
128
129     List<AnAction> actions = Lists.newArrayList(createActions(console, processHandler));
130
131     return new DefaultExecutionResult(console, processHandler, actions.toArray(new AnAction[actions.size()]));
132   }
133
134   @NotNull
135   protected ConsoleView createAndAttachConsole(Project project, ProcessHandler processHandler, Executor executor)
136     throws ExecutionException {
137     final ConsoleView consoleView = createConsoleBuilder(project).getConsole();
138     consoleView.addMessageFilter(new UrlFilter());
139
140     addTracebackFilter(project, consoleView, processHandler);
141
142     consoleView.attachToProcess(processHandler);
143     return consoleView;
144   }
145
146   protected void addTracebackFilter(Project project, ConsoleView consoleView, ProcessHandler processHandler) {
147     if (PySdkUtil.isRemote(myConfig.getSdk())) {
148       assert processHandler instanceof RemoteProcessHandlerBase;
149       consoleView
150         .addMessageFilter(new PyRemoteTracebackFilter(project, myConfig.getWorkingDirectory(), (RemoteProcessHandlerBase)processHandler));
151     }
152     else {
153       consoleView.addMessageFilter(new PythonTracebackFilter(project, myConfig.getWorkingDirectorySafe()));
154     }
155     consoleView.addMessageFilter(new UrlFilter()); // Url filter is always nice to have
156   }
157
158   private TextConsoleBuilder createConsoleBuilder(Project project) {
159     if (isDebug()) {
160       return new PyDebugConsoleBuilder(project, PythonSdkType.findSdkByPath(myConfig.getInterpreterPath()));
161     }
162     else {
163       return TextConsoleBuilderFactory.getInstance().createBuilder(project);
164     }
165   }
166
167   @Override
168   @NotNull
169   protected ProcessHandler startProcess() throws ExecutionException {
170     return startProcess(new CommandLinePatcher[]{});
171   }
172
173   /**
174    * Patches the command line parameters applying patchers from first to last, and then runs it.
175    *
176    * @param patchers any number of patchers; any patcher may be null, and the whole argument may be null.
177    * @return handler of the started process
178    * @throws ExecutionException
179    */
180   protected ProcessHandler startProcess(CommandLinePatcher... patchers) throws ExecutionException {
181     GeneralCommandLine commandLine = generateCommandLine(patchers);
182
183     // Extend command line
184     PythonRunConfigurationExtensionsManager.getInstance()
185       .patchCommandLine(myConfig, getRunnerSettings(), commandLine, getEnvironment().getRunner().getRunnerId());
186     Sdk sdk = PythonSdkType.findSdkByPath(myConfig.getInterpreterPath());
187     final ProcessHandler processHandler;
188     if (PySdkUtil.isRemote(sdk)) {
189       PyRemotePathMapper pathMapper = createRemotePathMapper();
190       processHandler = createRemoteProcessStarter().startRemoteProcess(sdk, commandLine, myConfig.getProject(), pathMapper);
191     }
192     else {
193       EncodingEnvironmentUtil.setLocaleEnvironmentIfMac(commandLine);
194       processHandler = doCreateProcess(commandLine);
195       ProcessTerminatedListener.attach(processHandler);
196     }
197
198     // attach extensions
199     PythonRunConfigurationExtensionsManager.getInstance().attachExtensionsToProcess(myConfig, processHandler, getRunnerSettings());
200
201     return processHandler;
202   }
203
204   @Nullable
205   private PyRemotePathMapper createRemotePathMapper() {
206     if (myConfig.getMappingSettings() == null) {
207       return null;
208     } else {
209       return PyRemotePathMapper.fromSettings(myConfig.getMappingSettings(), PyRemotePathMapper.PyPathMappingType.USER_DEFINED);
210     }
211   }
212
213   protected PyRemoteProcessStarter createRemoteProcessStarter() {
214     return new PyRemoteProcessStarter();
215   }
216
217
218   public GeneralCommandLine generateCommandLine(CommandLinePatcher[] patchers) throws ExecutionException {
219     GeneralCommandLine commandLine = generateCommandLine();
220     if (patchers != null) {
221       for (CommandLinePatcher patcher : patchers) {
222         if (patcher != null) patcher.patchCommandLine(commandLine);
223       }
224     }
225     return commandLine;
226   }
227
228   protected ProcessHandler doCreateProcess(GeneralCommandLine commandLine) throws ExecutionException {
229     return PythonProcessRunner.createProcess(commandLine);
230   }
231
232   public GeneralCommandLine generateCommandLine() throws ExecutionException {
233     GeneralCommandLine commandLine = createCommandLine();
234
235     commandLine.withCharset(EncodingProjectManager.getInstance(myConfig.getProject()).getDefaultCharset());
236
237     setRunnerPath(commandLine);
238
239     // define groups
240     createStandardGroupsIn(commandLine);
241
242     buildCommandLineParameters(commandLine);
243
244     initEnvironment(commandLine);
245     return commandLine;
246   }
247
248   private static GeneralCommandLine createCommandLine() {
249     return PtyCommandLine.isEnabled() ? new PtyCommandLine() : new GeneralCommandLine();
250   }
251
252   /**
253    * Creates a number of parameter groups in the command line:
254    * GROUP_EXE_OPTIONS, GROUP_DEBUGGER, GROUP_SCRIPT.
255    * These are necessary for command line patchers to work properly.
256    *
257    * @param commandLine
258    */
259   public static void createStandardGroupsIn(GeneralCommandLine commandLine) {
260     ParametersList params = commandLine.getParametersList();
261     params.addParamsGroup(GROUP_EXE_OPTIONS);
262     params.addParamsGroup(GROUP_DEBUGGER);
263     params.addParamsGroup(GROUP_PROFILER);
264     params.addParamsGroup(GROUP_SCRIPT);
265   }
266
267   protected void initEnvironment(GeneralCommandLine commandLine) {
268     Map<String, String> env = myConfig.getEnvs();
269     if (env == null) {
270       env = new HashMap<String, String>();
271     }
272     else {
273       env = new HashMap<String, String>(env);
274     }
275
276     addPredefinedEnvironmentVariables(env, myConfig.isPassParentEnvs());
277     addCommonEnvironmentVariables(env);
278
279     commandLine.getEnvironment().clear();
280     commandLine.getEnvironment().putAll(env);
281     commandLine.withParentEnvironmentType(myConfig.isPassParentEnvs() ? ParentEnvironmentType.SHELL : ParentEnvironmentType.NONE);
282
283     buildPythonPath(commandLine, myConfig.isPassParentEnvs());
284   }
285
286   protected static void addCommonEnvironmentVariables(Map<String, String> env) {
287     PythonEnvUtil.setPythonUnbuffered(env);
288     env.put("PYCHARM_HOSTED", "1");
289   }
290
291   public void addPredefinedEnvironmentVariables(Map<String, String> envs, boolean passParentEnvs) {
292     final PythonSdkFlavor flavor = PythonSdkFlavor.getFlavor(myConfig.getInterpreterPath());
293     if (flavor != null) {
294       flavor.addPredefinedEnvironmentVariables(envs, myConfig.getProject());
295     }
296   }
297
298   private void buildPythonPath(GeneralCommandLine commandLine, boolean passParentEnvs) {
299     Sdk pythonSdk = PythonSdkType.findSdkByPath(myConfig.getInterpreterPath());
300     if (pythonSdk != null) {
301       List<String> pathList = Lists.newArrayList(getAddedPaths(pythonSdk));
302       pathList.addAll(collectPythonPath());
303       initPythonPath(commandLine, passParentEnvs, pathList, myConfig.getInterpreterPath());
304     }
305   }
306
307   public static void initPythonPath(GeneralCommandLine commandLine,
308                                     boolean passParentEnvs,
309                                     List<String> pathList,
310                                     final String interpreterPath) {
311     final PythonSdkFlavor flavor = PythonSdkFlavor.getFlavor(interpreterPath);
312     if (flavor != null) {
313       flavor.initPythonPath(commandLine, pathList);
314     }
315     else {
316       PythonSdkFlavor.initPythonPath(commandLine.getEnvironment(), passParentEnvs, pathList);
317     }
318   }
319
320   public static List<String> getAddedPaths(Sdk pythonSdk) {
321     List<String> pathList = new ArrayList<String>();
322     final SdkAdditionalData sdkAdditionalData = pythonSdk.getSdkAdditionalData();
323     if (sdkAdditionalData instanceof PythonSdkAdditionalData) {
324       final Set<VirtualFile> addedPaths = ((PythonSdkAdditionalData)sdkAdditionalData).getAddedPathFiles();
325       for (VirtualFile file : addedPaths) {
326         addToPythonPath(file, pathList);
327       }
328     }
329     return pathList;
330   }
331
332   private static void addToPythonPath(VirtualFile file, Collection<String> pathList) {
333     if (file.getFileSystem() instanceof JarFileSystem) {
334       final VirtualFile realFile = JarFileSystem.getInstance().getVirtualFileForJar(file);
335       if (realFile != null) {
336         addIfNeeded(realFile, pathList);
337       }
338     }
339     else {
340       addIfNeeded(file, pathList);
341     }
342   }
343
344   private static void addIfNeeded(@NotNull final VirtualFile file, @NotNull final Collection<String> pathList) {
345     addIfNeeded(pathList, file.getPath());
346   }
347
348   protected static void addIfNeeded(Collection<String> pathList, String path) {
349     final Set<String> vals = Sets.newHashSet(pathList);
350     final String filePath = FileUtil.toSystemDependentName(path);
351     if (!vals.contains(filePath)) {
352       pathList.add(filePath);
353     }
354   }
355
356   protected Collection<String> collectPythonPath() {
357     final Module module = myConfig.getModule();
358     Set<String> pythonPath = Sets.newHashSet(collectPythonPath(module, myConfig.shouldAddContentRoots(), myConfig.shouldAddSourceRoots()));
359
360     if (isDebug() && getSdkFlavor() instanceof JythonSdkFlavor) { //that fixes Jython problem changing sys.argv on execfile, see PY-8164
361       pythonPath.add(PythonHelpersLocator.getHelperPath("pycharm"));
362       pythonPath.add(PythonHelpersLocator.getHelperPath("pydev"));
363     }
364
365     return pythonPath;
366   }
367
368   @NotNull
369   public static Collection<String> collectPythonPath(@Nullable Module module) {
370     return collectPythonPath(module, true, true);
371   }
372
373   @NotNull
374   public static Collection<String> collectPythonPath(@Nullable Module module, boolean addContentRoots,
375                                                      boolean addSourceRoots) {
376     Collection<String> pythonPathList = Sets.newLinkedHashSet();
377     if (module != null) {
378       Set<Module> dependencies = new HashSet<Module>();
379       ModuleUtilCore.getDependencies(module, dependencies);
380
381       if (addContentRoots) {
382         addRoots(pythonPathList, ModuleRootManager.getInstance(module).getContentRoots());
383         for (Module dependency : dependencies) {
384           addRoots(pythonPathList, ModuleRootManager.getInstance(dependency).getContentRoots());
385         }
386       }
387       if (addSourceRoots) {
388         addRoots(pythonPathList, ModuleRootManager.getInstance(module).getSourceRoots());
389         for (Module dependency : dependencies) {
390           addRoots(pythonPathList, ModuleRootManager.getInstance(dependency).getSourceRoots());
391         }
392       }
393
394       addLibrariesFromModule(module, pythonPathList);
395       addRootsFromModule(module, pythonPathList);
396       for (Module dependency : dependencies) {
397         addLibrariesFromModule(dependency, pythonPathList);
398         addRootsFromModule(dependency, pythonPathList);
399       }
400     }
401     return pythonPathList;
402   }
403
404   private static void addLibrariesFromModule(Module module, Collection<String> list) {
405     final OrderEntry[] entries = ModuleRootManager.getInstance(module).getOrderEntries();
406     for (OrderEntry entry : entries) {
407       if (entry instanceof LibraryOrderEntry) {
408         final String name = ((LibraryOrderEntry)entry).getLibraryName();
409         if (name != null && name.endsWith(LibraryContributingFacet.PYTHON_FACET_LIBRARY_NAME_SUFFIX)) {
410           // skip libraries from Python facet
411           continue;
412         }
413         for (VirtualFile root : ((LibraryOrderEntry)entry).getRootFiles(OrderRootType.CLASSES)) {
414           final Library library = ((LibraryOrderEntry)entry).getLibrary();
415           if (!PlatformUtils.isPyCharm()) {
416             addToPythonPath(root, list);
417           }
418           else if (library instanceof LibraryImpl) {
419             final PersistentLibraryKind<?> kind = ((LibraryImpl)library).getKind();
420             if (kind == PythonLibraryType.getInstance().getKind()) {
421               addToPythonPath(root, list);
422             }
423           }
424         }
425       }
426     }
427   }
428
429   private static void addRootsFromModule(Module module, Collection<String> pythonPathList) {
430
431     // for Jython
432     final CompilerModuleExtension extension = CompilerModuleExtension.getInstance(module);
433     if (extension != null) {
434       final VirtualFile path = extension.getCompilerOutputPath();
435       if (path != null) {
436         pythonPathList.add(path.getPath());
437       }
438       final VirtualFile pathForTests = extension.getCompilerOutputPathForTests();
439       if (pathForTests != null) {
440         pythonPathList.add(pathForTests.getPath());
441       }
442     }
443
444     //additional paths from facets (f.e. buildout)
445     final Facet[] facets = FacetManager.getInstance(module).getAllFacets();
446     for (Facet facet : facets) {
447       if (facet instanceof PythonPathContributingFacet) {
448         List<String> more_paths = ((PythonPathContributingFacet)facet).getAdditionalPythonPath();
449         if (more_paths != null) pythonPathList.addAll(more_paths);
450       }
451     }
452   }
453
454   private static void addRoots(Collection<String> pythonPathList, VirtualFile[] roots) {
455     for (VirtualFile root : roots) {
456       addToPythonPath(root, pythonPathList);
457     }
458   }
459
460   protected void setRunnerPath(GeneralCommandLine commandLine) throws ExecutionException {
461     String interpreterPath = getInterpreterPath();
462     commandLine.setExePath(FileUtil.toSystemDependentName(interpreterPath));
463   }
464
465   protected String getInterpreterPath() throws ExecutionException {
466     String interpreterPath = myConfig.getInterpreterPath();
467     if (interpreterPath == null) {
468       throw new ExecutionException("Cannot find Python interpreter for this run configuration");
469     }
470     return interpreterPath;
471   }
472
473   protected void buildCommandLineParameters(GeneralCommandLine commandLine) {
474   }
475
476   public boolean isMultiprocessDebug() {
477     if (myMultiprocessDebug != null) {
478       return myMultiprocessDebug;
479     }
480     else {
481       return PyDebuggerOptionsProvider.getInstance(myConfig.getProject()).isAttachToSubprocess();
482     }
483   }
484
485   public void setMultiprocessDebug(boolean multiprocessDebug) {
486     myMultiprocessDebug = multiprocessDebug;
487   }
488 }