mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[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:
committed by
intellij-monorepo-bot
parent
a255301b03
commit
1063443fdc
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+8
-43
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user