From 382a86c6e110f55fa3c537609824af92bf74055d Mon Sep 17 00:00:00 2001 From: "Ilya.Kazakevich" Date: Fri, 5 May 2023 12:25:02 +0000 Subject: [PATCH] Cleanup and document WSL network connectivity for cases like PY-59150. * Unify host and WSL fetching logic * Handle error explicitly by exception * Extract messages * Log stdout/stderr to logs, not to show em in dialogs * Test added Cleanup and document WSL network connectivity for cases like PY-59150. * No need to deal with IP address obtaining problems each time: encapsulate it in ``WslDistribution``. * Use one registry key to switch to ``127.0.0.1`` for Windows To Linux connection * Document current approach and usage * Make methods not nullable (some usages do not check null at all) Merge-request: IJ-MR-106936 Merged-by: Ilya Kazakevich GitOrigin-RevId: 8bb9415ea9859e76365dff79a57d2b4661897334 --- .../server/WslBuildCommandLineBuilder.java | 17 +- .../messages/JavaCompilerBundle.properties | 1 - .../wsl/target/WslTargetEnvironment.kt | 2 +- .../resources/messages/IdeBundle.properties | 6 + .../intellij/execution/wsl/IpOrException.kt | 12 ++ .../execution/wsl/WSLDistribution.java | 171 ++++++++++++------ .../intellij/execution/wsl/WslNetworkTest.kt | 46 +++++ .../wsl/GradleOnWslExecutionAware.kt | 2 +- .../wsl/WslMavenServerRemoteProcessSupport.kt | 9 +- .../messages/MavenSyncBundle.properties | 1 - 10 files changed, 187 insertions(+), 80 deletions(-) create mode 100644 platform/platform-impl/src/com/intellij/execution/wsl/IpOrException.kt create mode 100644 platform/platform-tests/testSrc/com/intellij/execution/wsl/WslNetworkTest.kt diff --git a/java/compiler/impl/src/com/intellij/compiler/server/WslBuildCommandLineBuilder.java b/java/compiler/impl/src/com/intellij/compiler/server/WslBuildCommandLineBuilder.java index 7a4cae23ecce..7a010f024dc1 100644 --- a/java/compiler/impl/src/com/intellij/compiler/server/WslBuildCommandLineBuilder.java +++ b/java/compiler/impl/src/com/intellij/compiler/server/WslBuildCommandLineBuilder.java @@ -24,7 +24,6 @@ import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; import java.util.List; @@ -57,7 +56,8 @@ final class WslBuildCommandLineBuilder implements BuildCommandLineBuilder { LOG.warn("ClasspathDirectory and myHostClasspathDirectory set to null!"); myClasspathDirectory = null; myHostClasspathDirectory = null; - } else { + } + else { myHostWorkingDirectory = buildDirectory.toString(); myWorkingDirectory = myDistribution.getWslPath(myHostWorkingDirectory); myClasspathDirectory = myWorkingDirectory + "/jps-" + ApplicationInfo.getInstance().getBuild().asString(); @@ -150,16 +150,17 @@ final class WslBuildCommandLineBuilder implements BuildCommandLineBuilder { @Override public InetAddress getListenAddress() { - return myDistribution.getHostIpAddress(); + try { + return myDistribution.getHostIpAddress(); + } + catch (ExecutionException ignored) { + return null; + } } @Override public @NotNull String getHostIp() throws ExecutionException { - String hostIp = myDistribution.getHostIp(); - if (hostIp == null) { - throw new ExecutionException(JavaCompilerBundle.message("dialog.message.failed.to.determine.host.ip.for.wsl.jdk")); - } - return hostIp; + return myDistribution.getHostIpAddress().getHostAddress(); } @Override diff --git a/java/compiler/openapi/resources/messages/JavaCompilerBundle.properties b/java/compiler/openapi/resources/messages/JavaCompilerBundle.properties index e4484cd177fe..9ca306547cc0 100644 --- a/java/compiler/openapi/resources/messages/JavaCompilerBundle.properties +++ b/java/compiler/openapi/resources/messages/JavaCompilerBundle.properties @@ -326,7 +326,6 @@ notification.action.jps.open.configuration.dialog=Configure... notification.title.jps.cannot.start.compiler=Cannot start compiler notification.title.cpu.snapshot.build.has.been.captured=Build CPU snapshot has been captured action.show.snapshot.location.text=Show Snapshot Location -dialog.message.failed.to.determine.host.ip.for.wsl.jdk=Failed to determine host IP for WSL JDK progress.preparing.wsl.build.environment=Preparing WSL build environment... plugins.advertiser.feature.artifact=artifact notification.group.compiler=Build finished diff --git a/platform/execution-impl/src/com/intellij/execution/wsl/target/WslTargetEnvironment.kt b/platform/execution-impl/src/com/intellij/execution/wsl/target/WslTargetEnvironment.kt index 8cfda10d9c81..fbbe7af71e5d 100644 --- a/platform/execution-impl/src/com/intellij/execution/wsl/target/WslTargetEnvironment.kt +++ b/platform/execution-impl/src/com/intellij/execution/wsl/target/WslTargetEnvironment.kt @@ -87,7 +87,7 @@ class WslTargetEnvironment constructor(override val request: WslTargetEnvironmen // TODO Breaks encapsulation. Instead, targetPortBinding should contain hosts to connect to. fun getWslIpAddress(): String = - distribution.wslIp + distribution.wslIpAddress.hostAddress private fun getWslPort(localPort: Int): Int { proxies[localPort]?.wslIngressPort?.let { diff --git a/platform/platform-api/resources/messages/IdeBundle.properties b/platform/platform-api/resources/messages/IdeBundle.properties index 4bfb8d39682c..4a10305864df 100644 --- a/platform/platform-api/resources/messages/IdeBundle.properties +++ b/platform/platform-api/resources/messages/IdeBundle.properties @@ -2525,6 +2525,12 @@ wsl.target.tool.step.description=WSL configuration wsl.opening_wsl=Opening WSL\u2026 wsl.no_path=Cannot find the Windows-specific part of this distribution, cannot browse it wsl.executing.process=Executing WSL Process +# {0} is one of ``wsl.*.ip`` phrases, see below +wsl.cant.parse.ip.process.failed=Could not parse {0} because of process failure. See idea.log for the particular error. +wsl.cant.parse.ip.no.output=Could not parse {0} because output didn't contain enough data. See idea.log for the particular error. +wsl.wsl.ip=WSL (Linux) IP +wsl.win.ip=Host (Windows) IP + name.variable=File name entered in the dialog settings.entry.point.tooltip=IDE and Project Settings diff --git a/platform/platform-impl/src/com/intellij/execution/wsl/IpOrException.kt b/platform/platform-impl/src/com/intellij/execution/wsl/IpOrException.kt new file mode 100644 index 000000000000..ead30b72ce5d --- /dev/null +++ b/platform/platform-impl/src/com/intellij/execution/wsl/IpOrException.kt @@ -0,0 +1,12 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.execution.wsl + +import com.intellij.execution.ExecutionException + +internal class IpOrException private constructor(private val result: Result) { + constructor(ip: String) : this(Result.success(ip)) + constructor(error: ExecutionException) : this(Result.failure(error)) + + @Throws(ExecutionException::class) + fun getIp(): String = result.getOrThrow() +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/execution/wsl/WSLDistribution.java b/platform/platform-impl/src/com/intellij/execution/wsl/WSLDistribution.java index 32472813a667..60f473d5e109 100644 --- a/platform/platform-impl/src/com/intellij/execution/wsl/WSLDistribution.java +++ b/platform/platform-impl/src/com/intellij/execution/wsl/WSLDistribution.java @@ -12,6 +12,7 @@ import com.intellij.execution.process.*; import com.intellij.ide.IdeBundle; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.NlsContexts; import com.intellij.openapi.util.NlsSafe; import com.intellij.openapi.util.NullableLazyValue; import com.intellij.openapi.util.io.FileUtil; @@ -20,10 +21,7 @@ import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.util.text.Strings; import com.intellij.openapi.vfs.impl.wsl.WslConstants; -import com.intellij.util.Consumer; -import com.intellij.util.Functions; -import com.intellij.util.ObjectUtils; -import com.intellij.util.SystemProperties; +import com.intellij.util.*; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NonNls; @@ -38,6 +36,7 @@ import java.net.InetAddress; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.*; +import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Pattern; @@ -47,6 +46,21 @@ import static com.intellij.openapi.util.NullableLazyValue.lazyNullable; /** * Represents a single linux distribution in WSL, installed after Fall Creators Update * + *

On network connectivity

+ * WSL1 shares network stack and both sides may use ``127.0.0.1`` to connect to each other. + *
+ * On WSL2 both OSes have separate interfaces ("eth0" on Linux and "vEthernet (WSL)" on Windows). + * One can't connect from Linux to Windows because of Windows Firewall which you need to disable or accomplish with rule, because + * of that it is not recommended to connect to IJ from processes launched on WSL. + * In other words, you shouldn't use {@link #getHostIpAddress()} in most cases. + *

+ * If you cant avoid that, use {@link com.intellij.execution.wsl.WslProxy} which makes tunnel to solve firewall issues. + *
+ * Connecting from Windows to Linux is possible in most cases (see {@link #getWslIpAddress()} and in modern WSL2 you can even use + * ``127.0.0.1`` if port is not occupied on Windows side. + * VPNs might break eth0 connectivity on WSL side (see PY-59608). In this case, enable wsl.proxy.connect.localhost + * + * @see Microsoft guide * @see WSLUtil */ public class WSLDistribution implements AbstractWslDistribution { @@ -70,8 +84,25 @@ public class WSLDistribution implements AbstractWslDistribution { private final @NotNull WslDistributionDescriptor myDescriptor; private final @Nullable Path myExecutablePath; private @Nullable Integer myVersion; - private final NullableLazyValue myHostIp = lazyNullable(this::readHostIp); - private final NullableLazyValue myWslIp = lazyNullable(this::readWslIp); + + private final LazyInitializer.LazyValue myHostIp = new LazyInitializer.LazyValue<>(() -> { + try { + return new IpOrException(readHostIp()); + } + catch (ExecutionException e) { + return new IpOrException(e); + } + }); + private final LazyInitializer.LazyValue myWslIp = new LazyInitializer.LazyValue<>(() -> { + try { + return readWslIp(); + } + catch (ExecutionException ex) { + // See class doc, IP section + LOG.warn("Can't read WSL IP, will use default: 127.0.0.1", ex); + return "127.0.0.1"; + } + }); private final NullableLazyValue myShellPath = lazyNullable(this::readShellPath); private final NullableLazyValue myUserHomeProvider = lazyNullable(this::readUserHome); @@ -550,34 +581,40 @@ public class WSLDistribution implements AbstractWslDistribution { return WslConstants.UNC_PREFIX + myDescriptor.getMsId(); } + /** + * Windows IP address. See class doc before using it, because this is probably not what you are looking for. + * + * @throws ExecutionException if IP can't be obtained (see logs for more info) + */ + @NotNull + public final InetAddress getHostIpAddress() throws ExecutionException { + return InetAddresses.forString(myHostIp.get().getIp()); + } + + /** + * Linux IP address. See class doc IP section for more info. + */ + @NotNull + public final InetAddress getWslIpAddress() { + if (Registry.is("wsl.proxy.connect.localhost")) { + return InetAddress.getLoopbackAddress(); + } + return InetAddresses.forString(myWslIp.get()); + } + // https://docs.microsoft.com/en-us/windows/wsl/compare-versions#accessing-windows-networking-apps-from-linux-host-ip - public String getHostIp() { - return myHostIp.getValue(); - } - - public String getWslIp() { - return myWslIp.getValue(); - } - - public InetAddress getHostIpAddress() { - return InetAddresses.forString(getHostIp()); - } - - public InetAddress getWslIpAddress() { - return InetAddresses.forString(getWslIp()); - } - - private @Nullable String readHostIp() { + private @NotNull String readHostIp() throws ExecutionException { String wsl1LoopbackAddress = getWsl1LoopbackAddress(); if (wsl1LoopbackAddress != null) { return wsl1LoopbackAddress; } if (Registry.is("wsl.obtain.windows.host.ip.alternatively", true)) { - InetAddress wslAddr = getWslIpAddress(); // Connect to any port on WSL IP. The destination endpoint is not needed to be reachable as no real connection is established. // This transfers the socket into "connected" state including setting the local endpoint according to the system's routing table. // Works on Windows and Linux. try (DatagramSocket datagramSocket = new DatagramSocket()) { + // We always need eth0 ip, can't use 127.0.0.1 + InetAddress wslAddr = InetAddresses.forString(readWslIp()); // Any port in range [1, 0xFFFF] can be used. Port=0 is forbidden: https://datatracker.ietf.org/doc/html/rfc8085 // "A UDP receiver SHOULD NOT bind to port zero". // Java asserts "port != 0" since v15 (https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8240533). @@ -586,53 +623,68 @@ public class WSLDistribution implements AbstractWslDistribution { return datagramSocket.getLocalAddress().getHostAddress(); } catch (Exception e) { - LOG.error("Cannot obtain Windows host IP alternatively: failed to connect to WSL IP " + wslAddr + ". Fallback to default way.", e); + LOG.error("Cannot obtain Windows host IP alternatively: failed to connect to WSL IP. Fallback to default way.", e); } } - final String releaseInfo = "/etc/resolv.conf"; // available for all distributions - final ProcessOutput output; - try { - output = executeOnWsl(List.of("cat", releaseInfo), new WSLCommandLineOptions(), 10_000, null); - } - catch (ExecutionException e) { - LOG.info("Cannot read host ip", e); + + return executeAndParseOutput(IdeBundle.message("wsl.win.ip"), strings -> { + for (String line : strings) { + if (line.startsWith("nameserver")) { + return line.substring("nameserver".length()).trim(); + } + } return null; - } - if (LOG.isDebugEnabled()) LOG.debug("Reading release info: " + getId()); - if (!output.checkSuccess(LOG)) return null; - for (String line : output.getStdoutLines(true)) { - if (line.startsWith("nameserver")) { - return line.substring("nameserver".length()).trim(); - } - } - return null; + }, "cat", "/etc/resolv.conf"); } - private @Nullable String readWslIp() { + private @NotNull String readWslIp() throws ExecutionException { String wsl1LoopbackAddress = getWsl1LoopbackAddress(); if (wsl1LoopbackAddress != null) { return wsl1LoopbackAddress; } - final ProcessOutput output; - try { - output = executeOnWsl(List.of("ip", "addr", "show", "eth0"), new WSLCommandLineOptions(), 10_000, null); - } - catch (ExecutionException e) { - LOG.info("Cannot read wsl ip", e); - return null; - } - if (LOG.isDebugEnabled()) LOG.debug("Reading eth0 info: " + getId()); - if (!output.checkSuccess(LOG)) return null; - for (String line : output.getStdoutLines(true)) { - String trimmed = line.trim(); - if (trimmed.startsWith("inet ")) { - int index = trimmed.indexOf("/"); - if (index != -1) { - return trimmed.substring("inet ".length(), index); + + return executeAndParseOutput(IdeBundle.message("wsl.wsl.ip"), strings -> { + for (String line : strings) { + String trimmed = line.trim(); + if (trimmed.startsWith("inet ")) { + int index = trimmed.indexOf("/"); + if (index != -1) { + return trimmed.substring("inet ".length(), index); + } } } + return null; + }, "ip", "addr", "show", "eth0"); + } + + /** + * Run command on WSL and parse IP from it + * + * @param ipType WSL or Windows + * @param parser block that accepts stdout and parses IP from it + * @param command command to run on WSL + * @return IP + * @throws ExecutionException IP can't be parsed + */ + @NotNull + private String executeAndParseOutput(@NlsContexts.DialogMessage @NotNull String ipType, + @NotNull Function, @Nullable String> parser, + @NotNull String @NotNull ... command) + throws ExecutionException { + final ProcessOutput output; + output = executeOnWsl(Arrays.asList(command), new WSLCommandLineOptions(), 10_000, null); + if (LOG.isDebugEnabled()) LOG.debug(ipType + " " + getId()); + if (!output.checkSuccess(LOG)) { + LOG.warn(String.format("%s. Exit code: %s. Error %s", ipType, output.getExitCode(), output.getStderr())); + throw new ExecutionException(IdeBundle.message("wsl.cant.parse.ip.no.output", ipType)); } - return null; + var stdout = output.getStdoutLines(true); + var ip = parser.apply(stdout); + if (ip != null) { + return ip; + } + LOG.warn(String.format("Can't parse data for %s, stdout is %s", ipType, String.join("\n", stdout))); + throw new ExecutionException(IdeBundle.message("wsl.cant.parse.ip.process.failed", ipType)); } private @Nullable String getWsl1LoopbackAddress() { @@ -657,5 +709,4 @@ public class WSLDistribution implements AbstractWslDistribution { return WslExecution.executeInShellAndGetCommandOnlyStdout(this, new GeneralCommandLine("printenv", "SHELL"), options, DEFAULT_TIMEOUT, true); } - } diff --git a/platform/platform-tests/testSrc/com/intellij/execution/wsl/WslNetworkTest.kt b/platform/platform-tests/testSrc/com/intellij/execution/wsl/WslNetworkTest.kt new file mode 100644 index 000000000000..70f8b729d91b --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/execution/wsl/WslNetworkTest.kt @@ -0,0 +1,46 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.execution.wsl + +import com.intellij.openapi.util.registry.Registry +import com.intellij.openapi.util.registry.withValue +import com.intellij.testFramework.RuleChain +import com.intellij.testFramework.fixtures.TestFixtureRule +import org.junit.Assert.assertTrue +import org.junit.ClassRule +import org.junit.Test +import kotlin.test.assertEquals + +/** + * Check that we can get Linux and Windows IPs for WSL + */ +class WslNetworkTest { + companion object { + private val appRule = TestFixtureRule() + private val wslRule = WslRule() + + @ClassRule + @JvmField + val ruleChain: RuleChain = RuleChain(appRule, wslRule) + } + + @Test + fun testWslIp() { + assertTrue("Wrong WSL IP", wslRule.wsl.wslIpAddress.hostAddress.startsWith("172.")) + } + + @Test + fun testWslIpLocal() { + Registry.get("wsl.proxy.connect.localhost").withValue(true) { + assertEquals("127.0.0.1", wslRule.wsl.wslIpAddress.hostAddress, "Wrong WSL address") + } + } + + @Test + fun testWslHostIp() { + for (alt in arrayOf(true, false)) { + Registry.get("wsl.obtain.windows.host.ip.alternatively").withValue(alt) { + assertTrue("Wrong host IP", wslRule.wsl.hostIpAddress.hostAddress.startsWith("172.")) + } + } + } +} \ No newline at end of file diff --git a/plugins/gradle/src/org/jetbrains/plugins/gradle/service/execution/wsl/GradleOnWslExecutionAware.kt b/plugins/gradle/src/org/jetbrains/plugins/gradle/service/execution/wsl/GradleOnWslExecutionAware.kt index 47d710fd20cb..cf2085fc6730 100644 --- a/plugins/gradle/src/org/jetbrains/plugins/gradle/service/execution/wsl/GradleOnWslExecutionAware.kt +++ b/plugins/gradle/src/org/jetbrains/plugins/gradle/service/execution/wsl/GradleOnWslExecutionAware.kt @@ -107,7 +107,7 @@ class GradleOnWslExecutionAware : GradleExecutionAware { } override val pathMapper = getTargetPathMapper(wslDistribution) override fun getServerBindingAddress(targetEnvironmentConfiguration: TargetEnvironmentConfiguration): HostPort { - return HostPort(wslDistribution.wslIp, 0) + return HostPort(wslDistribution.wslIpAddress.hostAddress, 0) } } } diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/wsl/WslMavenServerRemoteProcessSupport.kt b/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/wsl/WslMavenServerRemoteProcessSupport.kt index af453613c7de..ea56681ccfee 100644 --- a/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/wsl/WslMavenServerRemoteProcessSupport.kt +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/wsl/WslMavenServerRemoteProcessSupport.kt @@ -6,7 +6,6 @@ import com.intellij.execution.configurations.RunProfileState import com.intellij.execution.wsl.WSLDistribution import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk -import org.jetbrains.idea.maven.execution.SyncBundle import org.jetbrains.idea.maven.server.AbstractMavenServerRemoteProcessSupport import org.jetbrains.idea.maven.server.WslMavenDistribution @@ -22,13 +21,7 @@ internal class WslMavenServerRemoteProcessSupport(private val myWslDistribution: return WslMavenCmdState(myWslDistribution, myJdk, myOptions, myDistribution as WslMavenDistribution, myDebugPort, myProject, remoteHost) } - override fun getRemoteHost(): String { - val ip = myWslDistribution.wslIp - if (ip == null) { - throw RuntimeException(SyncBundle.message("maven.sync.wsl.ip.cannot.resolve")) - } - return ip - } + override fun getRemoteHost(): String = myWslDistribution.wslIpAddress.hostAddress override fun type() = "WSL" } diff --git a/plugins/maven/src/main/resources/messages/MavenSyncBundle.properties b/plugins/maven/src/main/resources/messages/MavenSyncBundle.properties index 191176f7dd32..74ba4749404b 100644 --- a/plugins/maven/src/main/resources/messages/MavenSyncBundle.properties +++ b/plugins/maven/src/main/resources/messages/MavenSyncBundle.properties @@ -96,7 +96,6 @@ maven.sync.wsl.jdk.set.to.project=For the correct importing process Maven JDK f maven.sync.wsl.jdk.revert.usr=Maven JDK for importer was not found, /usr/bin/java will be used if exists maven.sync.wsl.jdk.fix=Open Maven Importing settings maven.sync.wsl.userhome.cannot.resolve=Cannot resolve $HOME variable on WSL. Looks like the WSL integration process is hanging. Try to restart WSL or the host machine. -maven.sync.wsl.ip.cannot.resolve=Cannot resolve WSL IP address. Looks like the WSL integration process is hanging. Try to restart WSL or the host machine. maven.cannot.reconnect=Cannot connect to the Maven process. If the problem persists, check network and local machine settings. See this article for details: https://intellij-support.jetbrains.com/hc/en-us/articles/360014262940