PY-73432 Run Python debugger in server mode by default

GitOrigin-RevId: 3cdce22120868761d823402bd6bcf660224591b8
This commit is contained in:
Andrey Lisin
2024-08-22 15:18:34 +02:00
committed by intellij-monorepo-bot
parent 37366e6018
commit 84a016fd62
4 changed files with 170 additions and 66 deletions

View File

@@ -230,6 +230,14 @@ fun TargetEnvironment.LocalPortBinding.getTargetEnvironmentValue(): TargetEnviro
resolvedPortBinding.targetEndpoint
}
fun TargetEnvironment.TargetPortBinding.getTargetEnvironmentValue(): TargetEnvironmentFunction<HostPort> =
TraceableTargetEnvironmentFunction { targetEnvironment ->
val targetPortBinding = this@getTargetEnvironmentValue
val resolvedPortBinding = (targetEnvironment.targetPortBindings[targetPortBinding]
?: throw IllegalStateException("Target port binding \"$targetPortBinding\" cannot be found"))
resolvedPortBinding.targetEndpoint
}
@Throws(IOException::class)
fun TargetEnvironment.downloadFromTarget(localPath: Path, progressIndicator: ProgressIndicator) {
val localFileDir = localPath.parent

View File

@@ -456,9 +456,12 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
<registryKey defaultValue="true" description="Halt variable resolve threads on step/resume" restartRequired="false"
key="python.debug.halt.variable.resolve.threads.on.step.resume"/>
<registryKey defaultValue="false" description="Use a single port for communication between PyCharm and the debugger"
<registryKey defaultValue="true" description="Use a single port for communication between PyCharm and the debugger"
restartRequired="false" key="python.debug.use.single.port"/>
<registryKey defaultValue="29781" description="Default Python debugger port" restartRequired="false"
key="python.debug.default.port" />
<!-- typing -->
<multiHostInjector implementation="com.jetbrains.python.codeInsight.typing.PyTypingAnnotationInjector"/>

View File

@@ -37,7 +37,6 @@ import com.intellij.openapi.roots.OrderRootType;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.registry.RegistryManager;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.ExperimentalUI;
@@ -111,6 +110,8 @@ public class PyDebugRunner implements ProgramRunner<RunnerSettings> {
private static final @NonNls String PYTHON3_PYCACHE_PREFIX_OPTION = "pycache_prefix=";
private static final int DEFAULT_DEBUGGER_PORT = 29781;
private static final Logger LOG = Logger.getInstance(PyDebugRunner.class);
@Override
@@ -168,33 +169,57 @@ public class PyDebugRunner implements ProgramRunner<RunnerSettings> {
final @NotNull ExecutionEnvironment environment) {
PythonCommandLineState pyState = (PythonCommandLineState)state;
RunProfile profile = environment.getRunProfile();
var clientId = ClientId.getCurrentOrNull();
return Promises
.runAsync(() -> {
int serverLocalPort = findAvailableSocketPort();
try {
PythonDebuggerScriptTargetedCommandLineBuilder debuggerScriptCommandLineBuilder =
new PythonDebuggerScriptTargetedCommandLineBuilder(environment.getProject(), pyState, profile, serverLocalPort);
ExecutionResult result = pyState.execute(environment.getExecutor(), debuggerScriptCommandLineBuilder);
ServerSocket serverSocket = debuggerScriptCommandLineBuilder.getServerSocketForDebugging();
if (serverSocket == null) {
LOG.error("The server socket has not been created after the target environment preparation" +
", trying to fallback and create the server socket on the loopback address");
serverSocket = createServerSocketOnLoopbackAddress(serverLocalPort);
if (Registry.is("python.debug.use.single.port")) {
int port = Registry.intValue("python.debug.default.port", DEFAULT_DEBUGGER_PORT);
TargetEnvironment.TargetPortBinding targetPortBinding =
new TargetEnvironment.TargetPortBinding(port, port);
return Promises
.runAsync(() -> {
try {
var debuggerScriptCommandLineBuilder = new PythonDebuggerServerModeTargetedCommandLineBuilder(
environment.getProject(), pyState, profile, targetPortBinding);
return pyState.execute(environment.getExecutor(), debuggerScriptCommandLineBuilder);
}
return Pair.create(serverSocket, result);
}
catch (ExecutionException err) {
throw new RuntimeException(err.getMessage(), err);
}
})
.thenAsync(pair -> AppUIExecutor.onUiThread().submit(() -> {
try (AccessToken ignored = ClientId.withClientId(clientId)) {
ServerSocket serverSocket = pair.getFirst();
ExecutionResult result = pair.getSecond();
return createXDebugSession(environment, pyState, serverSocket, result);
}
}));
catch (ExecutionException err) {
throw new RuntimeException(err);
}
})
.thenAsync(result -> AppUIExecutor.onUiThread().submit(() -> {
return createXDebugSession(environment, port, result);
}));
}
else {
var clientId = ClientId.getCurrentOrNull();
return Promises
.runAsync(() -> {
int serverLocalPort = findAvailableSocketPort();
try {
TargetEnvironment.LocalPortBinding localPortBinding =
new TargetEnvironment.LocalPortBinding(serverLocalPort, null);
var debuggerScriptCommandLineBuilder = new PythonDebuggerClientModeTargetedCommandLineBuilder(
environment.getProject(), pyState, profile, localPortBinding);
ExecutionResult result = pyState.execute(environment.getExecutor(), debuggerScriptCommandLineBuilder);
ServerSocket serverSocket = debuggerScriptCommandLineBuilder.getServerSocketForDebugging();
if (serverSocket == null) {
LOG.error("The server socket has not been created after the target environment preparation" +
", trying to fallback and create the server socket on the loopback address");
serverSocket = createServerSocketOnLoopbackAddress(serverLocalPort);
}
return Pair.create(serverSocket, result);
}
catch (ExecutionException err) {
throw new RuntimeException(err.getMessage(), err);
}
})
.thenAsync(pair -> AppUIExecutor.onUiThread().submit(() -> {
try (AccessToken ignored = ClientId.withClientId(clientId)) {
ServerSocket serverSocket = pair.getFirst();
ExecutionResult result = pair.getSecond();
return createXDebugSession(environment, pyState, serverSocket, result);
}
}));
}
}
private static @NotNull ServerSocket createServerSocketOnLoopbackAddress(int serverLocalPort) {
@@ -237,6 +262,26 @@ public class PyDebugRunner implements ProgramRunner<RunnerSettings> {
return session;
}
private @NotNull XDebugSession createXDebugSession(@NotNull ExecutionEnvironment environment,
int serverPort, ExecutionResult result) throws ExecutionException {
XDebugSession session = XDebuggerManager.getInstance(environment.getProject()).
startSession(environment, new XDebugProcessStarter() {
@Override
public @NotNull XDebugProcess start(@NotNull XDebugSession session) {
PyDebugProcess pyDebugProcess = createDebugProcess(session, serverPort, result);
createConsoleCommunicationAndSetupActions(environment.getProject(), result, pyDebugProcess, session);
return pyDebugProcess;
}
});
if (ExperimentalUI.isNewUI()) {
session.getUI().getDefaults().initContentAttraction(DebuggerContentInfo.CONSOLE_CONTENT,
XDebuggerUIConstants.LAYOUT_VIEW_FINISH_CONDITION,
new LayoutAttractionPolicy.FocusOnce());
}
return session;
}
/**
* To be deprecated.
* <p>
@@ -280,6 +325,10 @@ public class PyDebugRunner implements ProgramRunner<RunnerSettings> {
pyState.isMultiprocessDebug());
}
protected @NotNull PyDebugProcess createDebugProcess(@NotNull XDebugSession session, int serverPort, ExecutionResult result) {
return new PyDebugProcess(session, result.getExecutionConsole(), result.getProcessHandler(), "localhost", serverPort);
}
/**
* Calling this method with an SSH interpreter would lead to an error, because this method is expected to be executed from EDT,
* and therefore it would try to create an SSH connection in EDT.
@@ -595,7 +644,12 @@ public class PyDebugRunner implements ProgramRunner<RunnerSettings> {
configureDebugEnvironment(project, new TargetEnvironmentController(debuggerScript.getEnvs(), request), runProfile,
isLocalTarget);
configureClientModeDebugConnectionParameters(debuggerScript, serverPortOnTarget);
if (Registry.is("python.debug.use.single.port")) {
configureServerModeDebugConnectionParameters(debuggerScript, serverPortOnTarget);
}
else {
configureClientModeDebugConnectionParameters(debuggerScript, serverPortOnTarget);
}
originalPythonScript.accept(new PythonExecution.Visitor() {
@Override
@@ -687,21 +741,19 @@ public class PyDebugRunner implements ProgramRunner<RunnerSettings> {
}
private static void applyRegistryFlags(@NotNull EnvironmentController environmentController) {
var registryManager = RegistryManager.getInstance();
if (registryManager.is("python.debug.low.impact.monitoring.api")) {
if (Registry.is("python.debug.low.impact.monitoring.api")) {
environmentController.putFixedValue(USE_LOW_IMPACT_MONITORING, "True");
}
if (!registryManager.is("python.debug.enable.cython.speedups")) {
if (!Registry.is("python.debug.enable.cython.speedups")) {
environmentController.putFixedValue(PYDEVD_USE_CYTHON, "NO");
}
if (registryManager.is("python.debug.enable.diagnostic.prints")) {
if (Registry.is("python.debug.enable.diagnostic.prints")) {
environmentController.putFixedValue(PYCHARM_DEBUG, "True");
}
if (registryManager.is("python.debug.halt.variable.resolve.threads.on.step.resume")) {
if (Registry.is("python.debug.halt.variable.resolve.threads.on.step.resume")) {
environmentController.putFixedValue(HALT_VARIABLE_RESOLVE_THREADS_ON_STEP_RESUME, "True");
}
}
@@ -856,10 +908,10 @@ public class PyDebugRunner implements ProgramRunner<RunnerSettings> {
* @param serverPortOnTarget the server
*/
static void configureServerModeDebugConnectionParameters(@NotNull PythonExecution debuggerScript,
@NotNull Function<TargetEnvironment, Integer> serverPortOnTarget) {
@NotNull Function<TargetEnvironment, HostPort> serverPortOnTarget) {
// --port
debuggerScript.addParameter(PORT_PARAM);
debuggerScript.addParameter(serverPortOnTarget.andThen(Object::toString));
debuggerScript.addParameter(serverPortOnTarget.andThen(HostPort::getPort).andThen(Object::toString));
// --file
debuggerScript.addParameter(FILE_PARAM);
}
@@ -891,45 +943,25 @@ public class PyDebugRunner implements ProgramRunner<RunnerSettings> {
}
}
private class PythonDebuggerScriptTargetedCommandLineBuilder implements PythonScriptTargetedCommandLineBuilder {
private abstract class PythonDebuggerTargetedCommandLineBuilder implements PythonScriptTargetedCommandLineBuilder {
private final @NotNull Project myProject;
private final @NotNull PythonCommandLineState myPyState;
private final @NotNull RunProfile myProfile;
private final int myIdeDebugServerLocalPort;
private volatile @Nullable ServerSocket myServerSocketForDebugging;
private PythonDebuggerScriptTargetedCommandLineBuilder(@NotNull Project project,
@NotNull PythonCommandLineState pyState,
@NotNull RunProfile profile,
int ideDebugServerPort) {
private PythonDebuggerTargetedCommandLineBuilder(@NotNull Project project,
@NotNull PythonCommandLineState pyState,
@NotNull RunProfile profile) {
myProject = project;
myPyState = pyState;
myProfile = profile;
myIdeDebugServerLocalPort = ideDebugServerPort;
}
@Override
public @NotNull PythonExecution build(@NotNull HelpersAwareTargetEnvironmentRequest helpersAwareTargetRequest,
public final @NotNull PythonExecution build(@NotNull HelpersAwareTargetEnvironmentRequest helpersAwareTargetRequest,
@NotNull PythonExecution pythonScript) {
TargetEnvironment.LocalPortBinding ideServerPortBinding = new TargetEnvironment.LocalPortBinding(myIdeDebugServerLocalPort, null);
helpersAwareTargetRequest.getTargetEnvironmentRequest().getLocalPortBindings().add(ideServerPortBinding);
helpersAwareTargetRequest.getTargetEnvironmentRequest().onEnvironmentPrepared((environment, indicator) -> {
try {
myServerSocketForDebugging = createServerSocketForDebugging(environment, ideServerPortBinding);
}
catch (IOException e) {
LOG.error("Unable to create server socket for debugging", e);
}
return null;
});
Function<TargetEnvironment, HostPort> ideServerPortBindingValue =
TargetEnvironmentFunctions.getTargetEnvironmentValue(ideServerPortBinding);
PythonScriptExecution debuggerScript =
prepareDebuggerScriptExecution(myProject, ideServerPortBindingValue, myPyState, pythonScript, myProfile,
helpersAwareTargetRequest);
Function<TargetEnvironment, HostPort> portBinding = createPortBinding(helpersAwareTargetRequest);
PythonExecution debuggerScript = prepareDebuggerScriptExecution(myProject, portBinding, myPyState, pythonScript, myProfile,
helpersAwareTargetRequest);
var configuredInterpreterParameters = myPyState.getConfiguredInterpreterParameters();
@@ -946,6 +978,9 @@ public class PyDebugRunner implements ProgramRunner<RunnerSettings> {
return debuggerScript;
}
protected abstract @NotNull Function<TargetEnvironment, HostPort> createPortBinding(
@NotNull HelpersAwareTargetEnvironmentRequest helpersAwareTargetRequest);
private List<String> createInterpreterParametersToPreventPycGenerationInHelpersDir(@NotNull List<String>
existingInterpreterParameters) {
var sdk = myPyState.getSdk();
@@ -997,6 +1032,41 @@ public class PyDebugRunner implements ProgramRunner<RunnerSettings> {
Files.createDirectories(pycacheDir);
return pycacheDir.toAbsolutePath();
}
}
/**
* Builder class for creating a command line configured for debugging Python scripts in the client mode, when
* the debugger process connects to an IDE.
*/
private final class PythonDebuggerClientModeTargetedCommandLineBuilder extends PythonDebuggerTargetedCommandLineBuilder {
private final @NotNull TargetEnvironment.LocalPortBinding myLocalPortBinding;
private volatile @Nullable ServerSocket myServerSocketForDebugging;
private PythonDebuggerClientModeTargetedCommandLineBuilder(@NotNull Project project,
@NotNull PythonCommandLineState pyState,
@NotNull RunProfile profile,
@NotNull TargetEnvironment.LocalPortBinding localPortBinding) {
super(project, pyState, profile);
myLocalPortBinding = localPortBinding;
}
@Override
protected @NotNull Function<TargetEnvironment, HostPort> createPortBinding(@NotNull HelpersAwareTargetEnvironmentRequest helpersAwareTargetRequest) {
helpersAwareTargetRequest.getTargetEnvironmentRequest().getLocalPortBindings().add(myLocalPortBinding);
helpersAwareTargetRequest.getTargetEnvironmentRequest().onEnvironmentPrepared((environment, indicator) -> {
try {
myServerSocketForDebugging = createServerSocketForDebugging(environment, myLocalPortBinding);
}
catch (IOException e) {
LOG.error("Unable to create server socket for debugging", e);
}
return null;
});
helpersAwareTargetRequest.getTargetEnvironmentRequest().getLocalPortBindings().add(myLocalPortBinding);
return TargetEnvironmentFunctions.getTargetEnvironmentValue(myLocalPortBinding);
}
private static @NotNull ServerSocket createServerSocketForDebugging(@NotNull TargetEnvironment environment,
@NotNull TargetEnvironment.LocalPortBinding ideServerPortBinding)
@@ -1025,4 +1095,26 @@ public class PyDebugRunner implements ProgramRunner<RunnerSettings> {
return myServerSocketForDebugging;
}
}
/**
* Builder class for creating a command line configured for debugging Python scripts in the server mode, when
* an IDE process connects to the debugger.
*/
private final class PythonDebuggerServerModeTargetedCommandLineBuilder extends PythonDebuggerTargetedCommandLineBuilder {
private final @NotNull TargetEnvironment.TargetPortBinding myTargetPortBinding;
private PythonDebuggerServerModeTargetedCommandLineBuilder(@NotNull Project project,
@NotNull PythonCommandLineState pyState,
@NotNull RunProfile profile,
@NotNull TargetEnvironment.TargetPortBinding targetPortBinding) {
super(project, pyState, profile);
myTargetPortBinding = targetPortBinding;
}
@Override
protected @NotNull Function<TargetEnvironment, HostPort> createPortBinding(@NotNull HelpersAwareTargetEnvironmentRequest helpersAwareTargetRequest) {
helpersAwareTargetRequest.getTargetEnvironmentRequest().getTargetPortBindings().add(myTargetPortBinding);
return TargetEnvironmentFunctions.getTargetEnvironmentValue(myTargetPortBinding);
}
}
}

View File

@@ -1,6 +1,7 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.debugger
import com.intellij.execution.target.HostPort
import com.intellij.execution.target.local.LocalTargetEnvironment
import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
import com.intellij.execution.target.value.constant
@@ -14,7 +15,7 @@ class PyDebugRunnerTest {
AutoCloseableSoftAssertions().use { softAssertions ->
val debuggerScriptExecution = PythonScriptExecution()
val localTargetEnvironment = LocalTargetEnvironment(LocalTargetEnvironmentRequest())
PyDebugRunner.configureServerModeDebugConnectionParameters(debuggerScriptExecution, constant(8787))
PyDebugRunner.configureServerModeDebugConnectionParameters(debuggerScriptExecution, constant(HostPort("", 8787)))
softAssertions
.assertThat(debuggerScriptExecution.pythonScriptPath)
.isNull()