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