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