Merge remote-tracking branch 'origin/master'
[idea/community.git] / python / src / com / jetbrains / python / debugger / PyDebugRunner.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.debugger;
17
18 import com.google.common.collect.Lists;
19 import com.intellij.execution.*;
20 import com.intellij.execution.configurations.*;
21 import com.intellij.execution.console.LanguageConsoleBuilder;
22 import com.intellij.execution.executors.DefaultDebugExecutor;
23 import com.intellij.execution.process.ProcessHandler;
24 import com.intellij.execution.runners.ExecutionEnvironment;
25 import com.intellij.execution.runners.GenericProgramRunner;
26 import com.intellij.execution.ui.ExecutionConsole;
27 import com.intellij.execution.ui.RunContentDescriptor;
28 import com.intellij.openapi.application.ApplicationManager;
29 import com.intellij.openapi.fileEditor.FileDocumentManager;
30 import com.intellij.openapi.project.Project;
31 import com.intellij.openapi.projectRoots.Sdk;
32 import com.intellij.openapi.roots.OrderRootType;
33 import com.intellij.openapi.roots.ProjectRootManager;
34 import com.intellij.openapi.util.text.StringUtil;
35 import com.intellij.openapi.vfs.VirtualFile;
36 import com.intellij.xdebugger.XDebugProcess;
37 import com.intellij.xdebugger.XDebugProcessStarter;
38 import com.intellij.xdebugger.XDebugSession;
39 import com.intellij.xdebugger.XDebuggerManager;
40 import com.jetbrains.python.PythonHelper;
41 import com.jetbrains.python.console.PythonConsoleView;
42 import com.jetbrains.python.console.PythonDebugConsoleCommunication;
43 import com.jetbrains.python.console.PythonDebugLanguageConsoleView;
44 import com.jetbrains.python.console.pydev.ConsoleCommunicationListener;
45 import com.jetbrains.python.debugger.settings.PyDebuggerSettings;
46 import com.jetbrains.python.run.AbstractPythonRunConfiguration;
47 import com.jetbrains.python.run.CommandLinePatcher;
48 import com.jetbrains.python.run.DebugAwareConfiguration;
49 import com.jetbrains.python.run.PythonCommandLineState;
50 import com.jetbrains.python.sdk.flavors.PythonSdkFlavor;
51 import org.jetbrains.annotations.NotNull;
52 import org.jetbrains.annotations.Nullable;
53
54 import java.io.File;
55 import java.net.ServerSocket;
56 import java.util.List;
57
58 /**
59  * @author yole
60  */
61 public class PyDebugRunner extends GenericProgramRunner {
62   public static final String PY_DEBUG_RUNNER = "PyDebugRunner";
63
64   @SuppressWarnings("SpellCheckingInspection")
65   public static final String DEBUGGER_MAIN = "pydev/pydevd.py";
66   public static final String CLIENT_PARAM = "--client";
67   public static final String PORT_PARAM = "--port";
68   public static final String FILE_PARAM = "--file";
69   public static final String MODULE_PARAM = "--module";
70   public static final String IDE_PROJECT_ROOTS = "IDE_PROJECT_ROOTS";
71   public static final String LIBRARY_ROOTS = "LIBRARY_ROOTS";
72   public static final String PYTHON_ASYNCIO_DEBUG = "PYTHONASYNCIODEBUG";
73   @SuppressWarnings("SpellCheckingInspection")
74   public static final String GEVENT_SUPPORT = "GEVENT_SUPPORT";
75   public static final String PYDEVD_FILTERS = "PYDEVD_FILTERS";
76   public static final String PYDEVD_FILTER_LIBRARIES = "PYDEVD_FILTER_LIBRARIES";
77   public static boolean isModule = false;
78
79   @Override
80   @NotNull
81   public String getRunnerId() {
82     return PY_DEBUG_RUNNER;
83   }
84
85   @Override
86   public boolean canRun(@NotNull final String executorId, @NotNull final RunProfile profile) {
87     if (!DefaultDebugExecutor.EXECUTOR_ID.equals(executorId)) {
88       // If not debug at all
89       return false;
90     }
91     /**
92      * Any python configuration is debuggable unless it explicitly declares itself as DebugAwareConfiguration and denies it
93      * with canRunUnderDebug == false
94      */
95
96     if (profile instanceof WrappingRunConfiguration) {
97       // If configuration is wrapper -- unwrap it and check
98       return isDebuggable(((WrappingRunConfiguration<?>)profile).getPeer());
99     }
100     return isDebuggable(profile);
101   }
102
103   private static boolean isDebuggable(@NotNull final RunProfile profile) {
104     if (profile instanceof DebugAwareConfiguration) {
105       // if configuration knows whether debug is allowed
106       return ((DebugAwareConfiguration)profile).canRunUnderDebug();
107     }
108     if (profile instanceof AbstractPythonRunConfiguration) {
109       // Any python configuration is debuggable
110       return true;
111     }
112     // No even a python configuration
113     return false;
114   }
115
116
117   protected XDebugSession createSession(@NotNull RunProfileState state, @NotNull final ExecutionEnvironment environment)
118     throws ExecutionException {
119     FileDocumentManager.getInstance().saveAllDocuments();
120
121     final PythonCommandLineState pyState = (PythonCommandLineState)state;
122     final ServerSocket serverSocket = PythonCommandLineState.createServerSocket();
123     final int serverLocalPort = serverSocket.getLocalPort();
124     RunProfile profile = environment.getRunProfile();
125     final ExecutionResult result =
126       pyState.execute(environment.getExecutor(), createCommandLinePatchers(environment.getProject(), pyState, profile, serverLocalPort));
127
128     return XDebuggerManager.getInstance(environment.getProject()).
129       startSession(environment, new XDebugProcessStarter() {
130         @Override
131         @NotNull
132         public XDebugProcess start(@NotNull final XDebugSession session) {
133           PyDebugProcess pyDebugProcess =
134             createDebugProcess(session, serverSocket, result, pyState);
135
136           createConsoleCommunicationAndSetupActions(environment.getProject(), result, pyDebugProcess, session);
137           return pyDebugProcess;
138         }
139       });
140   }
141
142   @NotNull
143   protected PyDebugProcess createDebugProcess(@NotNull XDebugSession session,
144                                               ServerSocket serverSocket,
145                                               ExecutionResult result,
146                                               PythonCommandLineState pyState) {
147     return new PyDebugProcess(session, serverSocket, result.getExecutionConsole(), result.getProcessHandler(),
148                               pyState.isMultiprocessDebug());
149   }
150
151   @Override
152   protected RunContentDescriptor doExecute(@NotNull RunProfileState state, @NotNull final ExecutionEnvironment environment)
153     throws ExecutionException {
154     XDebugSession session = createSession(state, environment);
155     initSession(session, state, environment.getExecutor());
156     return session.getRunContentDescriptor();
157   }
158
159   protected void initSession(XDebugSession session, RunProfileState state, Executor executor) {
160   }
161
162   public static int findIndex(List<String> paramList, String paramName) {
163     for (int i = 0; i < paramList.size(); i++) {
164       if (paramName.equals(paramList.get(i))) {
165         return i + 1;
166       }
167     }
168     return -1;
169   }
170
171   public static void createConsoleCommunicationAndSetupActions(@NotNull final Project project,
172                                                                @NotNull final ExecutionResult result,
173                                                                @NotNull PyDebugProcess debugProcess, @NotNull XDebugSession session) {
174     ExecutionConsole console = result.getExecutionConsole();
175     if (console instanceof PythonDebugLanguageConsoleView) {
176       ProcessHandler processHandler = result.getProcessHandler();
177
178       initDebugConsoleView(project, debugProcess, (PythonDebugLanguageConsoleView)console, processHandler, session);
179     }
180   }
181
182   public static PythonDebugConsoleCommunication initDebugConsoleView(Project project,
183                                                                      PyDebugProcess debugProcess,
184                                                                      PythonDebugLanguageConsoleView console,
185                                                                      ProcessHandler processHandler, final XDebugSession session) {
186     PythonConsoleView pythonConsoleView = console.getPydevConsoleView();
187     PythonDebugConsoleCommunication debugConsoleCommunication = new PythonDebugConsoleCommunication(project, debugProcess);
188
189     pythonConsoleView.setConsoleCommunication(debugConsoleCommunication);
190
191
192     PydevDebugConsoleExecuteActionHandler consoleExecuteActionHandler = new PydevDebugConsoleExecuteActionHandler(pythonConsoleView,
193                                                                                                                   processHandler,
194                                                                                                                   debugConsoleCommunication);
195     pythonConsoleView.setExecutionHandler(consoleExecuteActionHandler);
196
197     debugProcess.getSession().addSessionListener(consoleExecuteActionHandler);
198     new LanguageConsoleBuilder(pythonConsoleView).processHandler(processHandler).initActions(consoleExecuteActionHandler, "py");
199
200
201     debugConsoleCommunication.addCommunicationListener(new ConsoleCommunicationListener() {
202       @Override
203       public void commandExecuted(boolean more) {
204         session.rebuildViews();
205       }
206
207       @Override
208       public void inputRequested() {
209       }
210     });
211
212     return debugConsoleCommunication;
213   }
214
215   @Nullable
216   private static CommandLinePatcher createRunConfigPatcher(RunProfileState state, RunProfile profile) {
217     CommandLinePatcher runConfigPatcher = null;
218     if (state instanceof PythonCommandLineState && profile instanceof AbstractPythonRunConfiguration) {
219       runConfigPatcher = (AbstractPythonRunConfiguration)profile;
220     }
221     return runConfigPatcher;
222   }
223
224   public CommandLinePatcher[] createCommandLinePatchers(final Project project, final PythonCommandLineState state,
225                                                                RunProfile profile,
226                                                                final int serverLocalPort) {
227     return new CommandLinePatcher[]{createDebugServerPatcher(project, state, serverLocalPort), createRunConfigPatcher(state, profile)};
228   }
229
230   private CommandLinePatcher createDebugServerPatcher(final Project project,
231                                                              final PythonCommandLineState pyState,
232                                                              final int serverLocalPort) {
233     return new CommandLinePatcher() {
234
235       private void patchExeParams(ParametersList parametersList) {
236         // we should remove '-m' parameter, but notify debugger of it
237         // but we can't remove one parameter from group, so we create new parameters group
238         ParamsGroup newExeParams = new ParamsGroup(PythonCommandLineState.GROUP_EXE_OPTIONS);
239         int exeParamsIndex = parametersList.getParamsGroups().indexOf(
240           parametersList.getParamsGroup(PythonCommandLineState.GROUP_EXE_OPTIONS));
241         ParamsGroup exeParamsOld = parametersList.removeParamsGroup(exeParamsIndex);
242         isModule = false;
243         for (String param : exeParamsOld.getParameters()) {
244           if (!param.equals("-m")) {
245             newExeParams.addParameter(param);
246           }
247           else {
248             isModule = true;
249           }
250         }
251
252         parametersList.addParamsGroupAt(exeParamsIndex, newExeParams);
253       }
254
255
256       @Override
257       public void patchCommandLine(GeneralCommandLine commandLine) {
258         // script name is the last parameter; all other params are for python interpreter; insert just before name
259         ParametersList parametersList = commandLine.getParametersList();
260
261         @SuppressWarnings("ConstantConditions") @NotNull
262         ParamsGroup debugParams = parametersList.getParamsGroup(PythonCommandLineState.GROUP_DEBUGGER);
263
264         patchExeParams(parametersList);
265
266         @SuppressWarnings("ConstantConditions") @NotNull
267         ParamsGroup exeParams = parametersList.getParamsGroup(PythonCommandLineState.GROUP_EXE_OPTIONS);
268
269         final PythonSdkFlavor flavor = pyState.getSdkFlavor();
270         if (flavor != null) {
271           assert exeParams != null;
272           for (String option : flavor.getExtraDebugOptions()) {
273             exeParams.addParameter(option);
274           }
275         }
276
277         assert debugParams != null;
278         fillDebugParameters(project, debugParams, serverLocalPort, pyState, commandLine);
279       }
280     };
281   }
282
283   private void fillDebugParameters(@NotNull Project project,
284                                           @NotNull ParamsGroup debugParams,
285                                           int serverLocalPort,
286                                           @NotNull PythonCommandLineState pyState,
287                                           @NotNull GeneralCommandLine cmd) {
288     PythonHelper.DEBUGGER.addToGroup(debugParams, cmd);
289
290     configureDebugParameters(project, debugParams, pyState, cmd);
291
292     if (PyDebuggerOptionsProvider.getInstance(project).isSupportGeventDebugging()) {
293       cmd.getEnvironment().put(GEVENT_SUPPORT, "True");
294     }
295
296     PyDebuggerSettings debuggerSettings = PyDebuggerSettings.getInstance();
297     if (debuggerSettings.isSteppingFiltersEnabled()) {
298       cmd.getEnvironment().put(PYDEVD_FILTERS, debuggerSettings.getSteppingFiltersForProject(project));
299     }
300     if (debuggerSettings.isLibrariesFilterEnabled()) {
301       cmd.getEnvironment().put(PYDEVD_FILTER_LIBRARIES, "True");
302     }
303
304     addProjectRootsToEnv(project, cmd);
305     addSdkRootsToEnv(project, cmd);
306
307     final String[] debuggerArgs = new String[]{
308       CLIENT_PARAM, "127.0.0.1",
309       PORT_PARAM, String.valueOf(serverLocalPort),
310       FILE_PARAM
311     };
312     for (String s : debuggerArgs) {
313       debugParams.addParameter(s);
314     }
315   }
316
317   protected void configureDebugParameters(@NotNull Project project,
318                                         @NotNull ParamsGroup debugParams,
319                                         @NotNull PythonCommandLineState pyState,
320                                         @NotNull GeneralCommandLine cmd) {
321     if (pyState.isMultiprocessDebug()) {
322       //noinspection SpellCheckingInspection
323       debugParams.addParameter("--multiproc");
324     }
325
326     if (isModule) {
327       debugParams.addParameter("--module");
328     }
329
330     if (ApplicationManager.getApplication().isUnitTestMode()) {
331       debugParams.addParameter("--DEBUG");
332     }
333
334     if (PyDebuggerOptionsProvider.getInstance(project).isSaveCallSignatures()) {
335       debugParams.addParameter("--save-signatures");
336     }
337
338     if (PyDebuggerOptionsProvider.getInstance(project).isSupportQtDebugging()) {
339       debugParams.addParameter("--qt-support");
340     }
341   }
342
343   private static void addProjectRootsToEnv(@NotNull Project project, @NotNull GeneralCommandLine commandLine) {
344
345     List<String> roots = Lists.newArrayList();
346     for (VirtualFile contentRoot : ProjectRootManager.getInstance(project).getContentRoots()) {
347       roots.add(contentRoot.getPath());
348     }
349
350     commandLine.getEnvironment().put(IDE_PROJECT_ROOTS, StringUtil.join(roots, File.pathSeparator));
351   }
352
353   private static void addSdkRootsToEnv(@NotNull Project project, @NotNull GeneralCommandLine commandLine) {
354     final RunManager runManager = RunManager.getInstance(project);
355     final RunnerAndConfigurationSettings selectedConfiguration = runManager.getSelectedConfiguration();
356     if (selectedConfiguration != null) {
357       final RunConfiguration configuration = selectedConfiguration.getConfiguration();
358       if (configuration instanceof AbstractPythonRunConfiguration) {
359         AbstractPythonRunConfiguration runConfiguration = (AbstractPythonRunConfiguration)configuration;
360         final Sdk sdk = runConfiguration.getSdk();
361         if (sdk != null) {
362           List<String> roots = Lists.newArrayList();
363           for (VirtualFile contentRoot : sdk.getSdkModificator().getRoots(OrderRootType.CLASSES)) {
364             roots.add(contentRoot.getPath());
365           }
366           commandLine.getEnvironment().put(LIBRARY_ROOTS, StringUtil.join(roots, File.pathSeparator));
367         }
368       }
369     }
370   }
371 }