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