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