diff --git a/java/execution/impl/src/com/intellij/execution/TargetDebuggerConnection.kt b/java/execution/impl/src/com/intellij/execution/TargetDebuggerConnection.kt deleted file mode 100644 index f27a112fc5af..000000000000 --- a/java/execution/impl/src/com/intellij/execution/TargetDebuggerConnection.kt +++ /dev/null @@ -1,33 +0,0 @@ -// 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.intellij.execution - -import com.intellij.execution.configurations.RemoteConnection -import com.intellij.execution.target.TargetEnvironment - -/** - * Allows resolving the stored debugger connection configuration against [TargetEnvironment] and to obtain [RemoteConnection] with the - * resolved connection parameters for the IDE. - */ -internal class TargetDebuggerConnection( - private val remoteConnection: RemoteConnection, - val debuggerPortRequest: TargetEnvironment.TargetPortBinding, -) { - private var remoteConnectionResolved: Boolean = false - - fun resolveRemoteConnection(environment: TargetEnvironment) { - val (localEndpoint, _) = environment.targetPortBindings[debuggerPortRequest] - ?: error("Target port binding $debuggerPortRequest could not be found in the environment: $environment") - remoteConnection.apply { - debuggerHostName = localEndpoint.host - debuggerAddress = localEndpoint.port.toString() - } - remoteConnectionResolved = true - } - - fun getResolvedRemoteConnection(): RemoteConnection { - if (!remoteConnectionResolved) { - throw IllegalStateException("The connection parameters to the debugger must be resolved with the target environment") - } - return remoteConnection - } -} \ No newline at end of file diff --git a/java/execution/impl/src/com/intellij/execution/TargetDebuggerConnectionUtil.kt b/java/execution/impl/src/com/intellij/execution/TargetDebuggerConnectionUtil.kt index eec2bdf78a72..9f7bdb7814d7 100644 --- a/java/execution/impl/src/com/intellij/execution/TargetDebuggerConnectionUtil.kt +++ b/java/execution/impl/src/com/intellij/execution/TargetDebuggerConnectionUtil.kt @@ -5,88 +5,81 @@ import com.intellij.debugger.impl.RemoteConnectionBuilder import com.intellij.debugger.settings.DebuggerSettings import com.intellij.execution.configurations.JavaCommandLineState import com.intellij.execution.configurations.JavaParameters +import com.intellij.execution.configurations.RemoteConnection +import com.intellij.execution.eel.TargetDebuggerConnectionProxy import com.intellij.execution.executors.DefaultDebugExecutor -import com.intellij.execution.target.TargetEnvironment.TargetPortBinding import com.intellij.execution.target.TargetEnvironmentRequest import com.intellij.execution.target.java.JavaLanguageRuntimeConfiguration import com.intellij.execution.target.local.LocalTargetEnvironmentRequest +import com.intellij.openapi.Disposable +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.JavaSdkVersion +private val LOG = logger() + internal object TargetDebuggerConnectionUtil { - private fun requiredDebuggerTargetPort(javaCommandLineState: JavaCommandLineState, request: TargetEnvironmentRequest): Int? { - // TODO Checking for a specific target is a gap in the idea of API. This check was introduced because the Java debugger - // runs in the server mode for local targets and in the client mode for other targets. But why? - // Anyway, the server mode requires a remote TCP forwarding that can't always be acquired for the Docker target. - // Maybe replace this method with something like `if (!request.isLocalPortForwardingSupported())`? - return if ( - DefaultDebugExecutor.EXECUTOR_ID.equals(javaCommandLineState.environment.executor.getId(), ignoreCase = true) - && request !is LocalTargetEnvironmentRequest - ) { - 12345 - } - else { - null - } - } - /** - * Performs preliminary work to configure debugger connection parameters to - * start the Java process with. The method adds the debugger connection - * parameters to the provided [JavaCommandLineState]. Then it returns - * [TargetDebuggerConnection] object that could be used later to - * resolve the connection parameters from IDE side against created - * [TargetEnvironment]. + * Performs preliminary work to configure debugger connection parameters to start the Java process with. The method adds the debugger + * connection parameters to the provided [JavaCommandLineState]. Then it returns [RemoteConnection] object that could be used later to + * resolve the connection parameters from IDE side against created [TargetEnvironment]. * - * - * Does nothing and returns `null` for - * [LocalTargetEnvironmentRequest] or an executor other than - * [DefaultDebugExecutor]. + * Does nothing and returns `null` for local execution request or an executor other than [DefaultDebugExecutor]. * * @param javaCommandLineState the command line state that is going to be * modified * @param request the target environment request - * @return the constructed [TargetDebuggerConnection] object for - * further resolution of connection parameters from IDE side or `null` - * in the case of inappropriate [Executor] or the local type of the - * `request`. + * @return the constructed [RemoteConnection] object with all required parameters from IDE side or `null` + * in the case of inappropriate [Executor] or the local type of the `request`. */ @JvmStatic - fun prepareDebuggerConnection( - javaCommandLineState: JavaCommandLineState, - request: TargetEnvironmentRequest, - ): TargetDebuggerConnection? { - val javaParameters: JavaParameters = runCatching { - javaCommandLineState.javaParameters - }.getOrNull() ?: return null - - val remotePort = requiredDebuggerTargetPort(javaCommandLineState, request) ?: return null - - try { - val java9plus: Boolean = request.isJava9Plus() - - val remoteAddressForVmParams: String = if (java9plus) { - // IDEA-225182 - hack: pass "host:port" to construct correct VM params, then adjust the connection - // IDEA-265364 - enforce ipv4 here with explicit 0.0.0.0 address - "0.0.0.0:$remotePort" + fun prepareDebuggerConnection(javaCommandLineState: JavaCommandLineState, request: TargetEnvironmentRequest): RemoteConnection? { + if (javaCommandLineState.isDebugExecutor() && request !is LocalTargetEnvironmentRequest) { + try { + return prepareRemoteConnection( + javaCommandLineState.environment.project, + javaCommandLineState.environment, + javaCommandLineState.javaParameters, + request.isJava9Plus() + ) } - else { - remotePort.toString() + catch (e: Exception) { + LOG.error("Unable to prepare TargetDebuggerConnection", e) } - - val remoteConnection = RemoteConnectionBuilder(false, DebuggerSettings.SOCKET_TRANSPORT, remoteAddressForVmParams) - .suspend(true) - .create(javaParameters) - - remoteConnection.applicationAddress = remotePort.toString() - if (java9plus) { - remoteConnection.applicationHostName = "*" - } - - return TargetDebuggerConnection(remoteConnection, TargetPortBinding(null, remotePort)) } - catch (_: ExecutionException) { - return null + return null + } + + private fun JavaCommandLineState.isDebugExecutor(): Boolean = + DefaultDebugExecutor.EXECUTOR_ID.equals(environment.executor.getId(), ignoreCase = true) + + private fun prepareRemoteConnection( + project: Project, + disposable: Disposable, + javaParameters: JavaParameters, + isJava9Plus: Boolean, + ): RemoteConnection { + val (localPort, remotePort) = TargetDebuggerConnectionProxy.getProxy(project, disposable) + val remoteAddressForVmParams: String = if (isJava9Plus) { + // IDEA-225182 - hack: pass "host:port" to construct correct VM params, then adjust the connection + "0.0.0.0:${remotePort}" + } + else { + remotePort.toString() + } + + val remoteConnection = RemoteConnectionBuilder(false, DebuggerSettings.SOCKET_TRANSPORT, remoteAddressForVmParams) + .suspend(true) + .create(javaParameters) + + return remoteConnection.apply { + applicationAddress = remotePort.toString() + debuggerAddress = localPort.toString() + debuggerHostName = "localhost" + if (isJava9Plus) { + applicationHostName = "*" + } } } @@ -94,9 +87,6 @@ internal object TargetDebuggerConnectionUtil { val javaVersion = configuration?.runtimes ?.findByType(JavaLanguageRuntimeConfiguration::class.java) ?.javaVersionString ?: return false - if (javaVersion.isEmpty()) { - return false - } - return JavaSdkVersion.fromVersionString(javaVersion)?.isAtLeast(JavaSdkVersion.JDK_1_9) ?: false + return javaVersion.isNotEmpty() && JavaSdkVersion.fromVersionString(javaVersion)?.isAtLeast(JavaSdkVersion.JDK_1_9) ?: false } } diff --git a/java/execution/impl/src/com/intellij/execution/configurations/JavaCommandLineState.java b/java/execution/impl/src/com/intellij/execution/configurations/JavaCommandLineState.java index 6d67d283afa6..6756e9f59ba7 100644 --- a/java/execution/impl/src/com/intellij/execution/configurations/JavaCommandLineState.java +++ b/java/execution/impl/src/com/intellij/execution/configurations/JavaCommandLineState.java @@ -3,7 +3,6 @@ package com.intellij.execution.configurations; import com.intellij.execution.ExecutionBundle; import com.intellij.execution.ExecutionException; -import com.intellij.execution.TargetDebuggerConnection; import com.intellij.execution.TargetDebuggerConnectionUtil; import com.intellij.execution.process.OSProcessHandler; import com.intellij.execution.runners.ExecutionEnvironment; @@ -18,8 +17,6 @@ import com.intellij.execution.target.TargetedCommandLineBuilder; import com.intellij.execution.target.java.JavaLanguageRuntimeConfiguration; import com.intellij.execution.target.local.LocalTargetEnvironment; import com.intellij.execution.target.local.LocalTargetEnvironmentRequest; -import com.intellij.execution.wsl.WslPath; -import com.intellij.execution.wsl.target.WslTargetEnvironmentConfiguration; import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.projectRoots.Sdk; @@ -36,10 +33,11 @@ import java.util.Objects; public abstract class JavaCommandLineState extends CommandLineState implements JavaCommandLine, TargetEnvironmentAwareRunProfileState, RemoteConnectionCreator { private static final Logger LOG = Logger.getInstance(JavaCommandLineState.class); + private JavaParameters myParams; private TargetEnvironmentRequest myTargetEnvironmentRequest; private TargetedCommandLineBuilder myCommandLine; - private volatile @Nullable TargetDebuggerConnection myTargetDebuggerConnection; + private volatile @Nullable RemoteConnection myTargetDebuggerConnection; protected JavaCommandLineState(@NotNull ExecutionEnvironment environment) { super(environment); @@ -113,24 +111,6 @@ public abstract class JavaCommandLineState extends CommandLineState implements J return null; } - @ApiStatus.Obsolete - public static WslTargetEnvironmentConfiguration checkCreateWslConfiguration(@Nullable Sdk jdk) { - if (jdk == null) { - return null; - } - VirtualFile virtualFile = jdk.getHomeDirectory(); - if (virtualFile == null) { - return null; - } - WslPath wslPath = WslPath.parseWindowsUncPath(virtualFile.getPath()); - if (wslPath != null) { - WslTargetEnvironmentConfiguration config = new WslTargetEnvironmentConfiguration(wslPath.getDistribution()); - addJavaLangConfig(config, wslPath.getLinuxPath(), jdk); - return config; - } - return null; - } - private static void addJavaLangConfig(TargetEnvironmentConfiguration config, String javaHomePath, Sdk jdk) { JavaLanguageRuntimeConfiguration javaConfig = new JavaLanguageRuntimeConfiguration(); javaConfig.setHomePath(javaHomePath); @@ -148,40 +128,25 @@ public abstract class JavaCommandLineState extends CommandLineState implements J @Override public void prepareTargetEnvironmentRequest( @NotNull TargetEnvironmentRequest request, - @NotNull TargetProgressIndicator targetProgressIndicator) throws ExecutionException { + @NotNull TargetProgressIndicator targetProgressIndicator + ) throws ExecutionException { targetProgressIndicator.addSystemLine(ExecutionBundle.message("progress.text.prepare.target.requirements")); myTargetEnvironmentRequest = request; - - TargetDebuggerConnection targetDebuggerConnection = - shouldPrepareDebuggerConnection() ? TargetDebuggerConnectionUtil.prepareDebuggerConnection(this, request) : null; - myTargetDebuggerConnection = targetDebuggerConnection; - + myTargetDebuggerConnection = shouldPrepareDebuggerConnection() + ? TargetDebuggerConnectionUtil.prepareDebuggerConnection(this, request) + : null; myCommandLine = createTargetedCommandLine(myTargetEnvironmentRequest); - - if (targetDebuggerConnection != null) { - Objects.requireNonNull(request).getTargetPortBindings().add(targetDebuggerConnection.getDebuggerPortRequest()); - } } @Override public void handleCreatedTargetEnvironment(@NotNull TargetEnvironment environment, @NotNull TargetProgressIndicator targetProgressIndicator) { - TargetDebuggerConnection targetDebuggerConnection = myTargetDebuggerConnection; - if (targetDebuggerConnection != null) { - targetDebuggerConnection.resolveRemoteConnection(environment); - } } @Override public @Nullable RemoteConnection createRemoteConnection(ExecutionEnvironment environment) { - TargetDebuggerConnection targetDebuggerConnection = myTargetDebuggerConnection; - if (targetDebuggerConnection != null) { - return targetDebuggerConnection.getResolvedRemoteConnection(); - } - else { - return null; - } + return myTargetDebuggerConnection; } @Override diff --git a/java/execution/impl/src/com/intellij/execution/eel/TargetDebuggerConnectionProxy.kt b/java/execution/impl/src/com/intellij/execution/eel/TargetDebuggerConnectionProxy.kt new file mode 100644 index 000000000000..fe223cd498ee --- /dev/null +++ b/java/execution/impl/src/com/intellij/execution/eel/TargetDebuggerConnectionProxy.kt @@ -0,0 +1,89 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.execution.eel + +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.progress.runBlockingMaybeCancellable +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import com.intellij.platform.eel.channels.EelDelicateApi +import com.intellij.platform.eel.eelProxy +import com.intellij.platform.eel.provider.LocalEelDescriptor +import com.intellij.platform.eel.provider.getEelDescriptor +import com.intellij.platform.eel.provider.localEel +import com.intellij.platform.eel.provider.portAccessibleLocally.EelPortAccessibleLocally.Companion.isEelPortAccessibleLocally +import com.intellij.platform.eel.provider.toEelApi +import com.intellij.platform.eel.provider.utils.acceptOnTcpPort +import com.intellij.platform.eel.provider.utils.connectToTcpPort +import com.intellij.util.net.NetUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import java.util.concurrent.ThreadLocalRandom + +private val LOG = logger() + +internal object TargetDebuggerConnectionProxy { + + @Service(Service.Level.PROJECT) + private class ProxyCoroutineScopeHolder(val coroutineScope: CoroutineScope) + + private val Project.proxyCoroutineScope: CoroutineScope + get() = service().coroutineScope + + fun getProxy(project: Project, disposable: Disposable): Pair = runBlockingMaybeCancellable { + project.getDirectPort() ?: project.runTunnel(disposable) + } + + // a special case for WSL with a mirrored network mode + private suspend fun Project.getDirectPort(): Pair? { + val localPort = NetUtils.findAvailableSocketPort() + val localPortUShort = localPort.toUShort() + val eelDescriptor = getEelDescriptor() + if (eelDescriptor == LocalEelDescriptor || isEelPortAccessibleLocally(localPortUShort, localPortUShort, eelDescriptor)) { + return localPort to localPort + } + return null + } + + @OptIn(EelDelicateApi::class) + private suspend fun Project.runTunnel(disposable: Disposable): Pair { + val remoteTunnels = getEelDescriptor() + .toEelApi() + .tunnels + + val localPort = NetUtils.findAvailableSocketPort() + val remotePort = getEphemeralPort() + try { + val proxy = eelProxy() + .acceptOnTcpPort(localEel.tunnels, port = localPort.toUShort()) + .connectToTcpPort(remoteTunnels, port = remotePort.toUShort()) + .onConnection { LOG.info("Debugger proxy [$localPort : $remotePort] accepted an incoming connection") } + .onConnectionClosed { LOG.info("Debugger proxy [$localPort : $remotePort] closed a connection") } + .onConnectionError { LOG.error("A debugger proxy [$localPort : $remotePort] error occurred: ${it.message}") } + .eelIt() + val job = proxyCoroutineScope.launch { + try { + proxy.runForever() + } + finally { + LOG.info("An IJent proxy from $localPort to $remotePort was terminated") + } + } + Disposer.register(disposable) { + job.cancel() + } + return localPort to remotePort + } + catch (e: Exception) { + LOG.error("Unable to start a proxy from $localPort to $remotePort", e) + throw IllegalStateException("Unable to start a proxy from $localPort to $remotePort", e) + } + } + + // there is no fast and easy way to get a free port on the remote side + // 49152 - 65535 is the range suggested by IANA as a safe range for dynamic ports + private fun getEphemeralPort(): Int = ThreadLocalRandom.current().nextInt(49152, 65535) +} +