[IDEA-392752] use eel api to run java debugger on a remote target

(cherry picked from commit fe9ce07da6b71331393c32fca4e9b0d80c17ab20)

IJ-CR-219084

GitOrigin-RevId: b4ca91d8676e1df4c1d2b166563f9e92225d827d
This commit is contained in:
Alexander.Glukhov
2026-08-28 13:24:08 +00:00
committed by intellij-monorepo-bot
parent a255301b03
commit 1063443fdc
4 changed files with 154 additions and 143 deletions
@@ -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
}
}
@@ -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<TargetDebuggerConnectionUtil>()
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
}
}
@@ -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
@@ -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<TargetDebuggerConnectionProxy>()
internal object TargetDebuggerConnectionProxy {
@Service(Service.Level.PROJECT)
private class ProxyCoroutineScopeHolder(val coroutineScope: CoroutineScope)
private val Project.proxyCoroutineScope: CoroutineScope
get() = service<ProxyCoroutineScopeHolder>().coroutineScope
fun getProxy(project: Project, disposable: Disposable): Pair<Int, Int> = runBlockingMaybeCancellable {
project.getDirectPort() ?: project.runTunnel(disposable)
}
// a special case for WSL with a mirrored network mode
private suspend fun Project.getDirectPort(): Pair<Int, Int>? {
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<Int, Int> {
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)
}