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 <ilya.kazakevich@jetbrains.com>

GitOrigin-RevId: 8bb9415ea9859e76365dff79a57d2b4661897334
This commit is contained in:
Ilya.Kazakevich
2023-05-05 12:25:02 +00:00
committed by intellij-monorepo-bot
parent 963398ea2b
commit 382a86c6e1
10 changed files with 187 additions and 80 deletions

View File

@@ -24,7 +24,6 @@ import java.nio.charset.Charset;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime; import java.nio.file.attribute.FileTime;
import java.util.List; import java.util.List;
@@ -57,7 +56,8 @@ final class WslBuildCommandLineBuilder implements BuildCommandLineBuilder {
LOG.warn("ClasspathDirectory and myHostClasspathDirectory set to null!"); LOG.warn("ClasspathDirectory and myHostClasspathDirectory set to null!");
myClasspathDirectory = null; myClasspathDirectory = null;
myHostClasspathDirectory = null; myHostClasspathDirectory = null;
} else { }
else {
myHostWorkingDirectory = buildDirectory.toString(); myHostWorkingDirectory = buildDirectory.toString();
myWorkingDirectory = myDistribution.getWslPath(myHostWorkingDirectory); myWorkingDirectory = myDistribution.getWslPath(myHostWorkingDirectory);
myClasspathDirectory = myWorkingDirectory + "/jps-" + ApplicationInfo.getInstance().getBuild().asString(); myClasspathDirectory = myWorkingDirectory + "/jps-" + ApplicationInfo.getInstance().getBuild().asString();
@@ -150,16 +150,17 @@ final class WslBuildCommandLineBuilder implements BuildCommandLineBuilder {
@Override @Override
public InetAddress getListenAddress() { public InetAddress getListenAddress() {
return myDistribution.getHostIpAddress(); try {
return myDistribution.getHostIpAddress();
}
catch (ExecutionException ignored) {
return null;
}
} }
@Override @Override
public @NotNull String getHostIp() throws ExecutionException { public @NotNull String getHostIp() throws ExecutionException {
String hostIp = myDistribution.getHostIp(); return myDistribution.getHostIpAddress().getHostAddress();
if (hostIp == null) {
throw new ExecutionException(JavaCompilerBundle.message("dialog.message.failed.to.determine.host.ip.for.wsl.jdk"));
}
return hostIp;
} }
@Override @Override

View File

@@ -326,7 +326,6 @@ notification.action.jps.open.configuration.dialog=Configure...
notification.title.jps.cannot.start.compiler=Cannot start compiler notification.title.jps.cannot.start.compiler=Cannot start compiler
notification.title.cpu.snapshot.build.has.been.captured=Build CPU snapshot has been captured notification.title.cpu.snapshot.build.has.been.captured=Build CPU snapshot has been captured
action.show.snapshot.location.text=Show Snapshot Location 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... progress.preparing.wsl.build.environment=Preparing WSL build environment...
plugins.advertiser.feature.artifact=artifact plugins.advertiser.feature.artifact=artifact
notification.group.compiler=Build finished notification.group.compiler=Build finished

View File

@@ -87,7 +87,7 @@ class WslTargetEnvironment constructor(override val request: WslTargetEnvironmen
// TODO Breaks encapsulation. Instead, targetPortBinding should contain hosts to connect to. // TODO Breaks encapsulation. Instead, targetPortBinding should contain hosts to connect to.
fun getWslIpAddress(): String = fun getWslIpAddress(): String =
distribution.wslIp distribution.wslIpAddress.hostAddress
private fun getWslPort(localPort: Int): Int { private fun getWslPort(localPort: Int): Int {
proxies[localPort]?.wslIngressPort?.let { proxies[localPort]?.wslIngressPort?.let {

View File

@@ -2525,6 +2525,12 @@ wsl.target.tool.step.description=WSL configuration
wsl.opening_wsl=Opening WSL\u2026 wsl.opening_wsl=Opening WSL\u2026
wsl.no_path=Cannot find the Windows-specific part of this distribution, cannot browse it wsl.no_path=Cannot find the Windows-specific part of this distribution, cannot browse it
wsl.executing.process=Executing WSL Process 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 name.variable=File name entered in the dialog
settings.entry.point.tooltip=IDE and Project Settings settings.entry.point.tooltip=IDE and Project Settings

View File

@@ -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<String>) {
constructor(ip: String) : this(Result.success(ip))
constructor(error: ExecutionException) : this(Result.failure(error))
@Throws(ExecutionException::class)
fun getIp(): String = result.getOrThrow()
}

View File

@@ -12,6 +12,7 @@ import com.intellij.execution.process.*;
import com.intellij.ide.IdeBundle; import com.intellij.ide.IdeBundle;
import com.intellij.openapi.project.Project; import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key; import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.util.NlsSafe; import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.NullableLazyValue; import com.intellij.openapi.util.NullableLazyValue;
import com.intellij.openapi.util.io.FileUtil; 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.StringUtil;
import com.intellij.openapi.util.text.Strings; import com.intellij.openapi.util.text.Strings;
import com.intellij.openapi.vfs.impl.wsl.WslConstants; import com.intellij.openapi.vfs.impl.wsl.WslConstants;
import com.intellij.util.Consumer; import com.intellij.util.*;
import com.intellij.util.Functions;
import com.intellij.util.ObjectUtils;
import com.intellij.util.SystemProperties;
import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NonNls;
@@ -38,6 +36,7 @@ import java.net.InetAddress;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.regex.Pattern; 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 <a href="https://blogs.msdn.microsoft.com/commandline/2017/10/11/whats-new-in-wsl-in-windows-10-fall-creators-update/">Fall Creators Update</a> * Represents a single linux distribution in WSL, installed after <a href="https://blogs.msdn.microsoft.com/commandline/2017/10/11/whats-new-in-wsl-in-windows-10-fall-creators-update/">Fall Creators Update</a>
* *
* <h2>On network connectivity</h2>
* WSL1 shares network stack and both sides may use ``127.0.0.1`` to connect to each other.
* <br/>
* 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 <a href="https://www.jetbrains.com/help/idea/how-to-use-wsl-development-environment-in-product.html#debugging_system_settings">Windows Firewall which you need to disable or accomplish with rule</a>, 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.
* <p>
* If you cant avoid that, use {@link com.intellij.execution.wsl.WslProxy} which makes tunnel to solve firewall issues.
* <br/>
* 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 <code>wsl.proxy.connect.localhost</code>
*
* @see <a href="https://learn.microsoft.com/en-us/windows/wsl/networking">Microsoft guide</a>
* @see WSLUtil * @see WSLUtil
*/ */
public class WSLDistribution implements AbstractWslDistribution { public class WSLDistribution implements AbstractWslDistribution {
@@ -70,8 +84,25 @@ public class WSLDistribution implements AbstractWslDistribution {
private final @NotNull WslDistributionDescriptor myDescriptor; private final @NotNull WslDistributionDescriptor myDescriptor;
private final @Nullable Path myExecutablePath; private final @Nullable Path myExecutablePath;
private @Nullable Integer myVersion; private @Nullable Integer myVersion;
private final NullableLazyValue<String> myHostIp = lazyNullable(this::readHostIp);
private final NullableLazyValue<String> myWslIp = lazyNullable(this::readWslIp); private final LazyInitializer.LazyValue<IpOrException> myHostIp = new LazyInitializer.LazyValue<>(() -> {
try {
return new IpOrException(readHostIp());
}
catch (ExecutionException e) {
return new IpOrException(e);
}
});
private final LazyInitializer.LazyValue<String> 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<String> myShellPath = lazyNullable(this::readShellPath); private final NullableLazyValue<String> myShellPath = lazyNullable(this::readShellPath);
private final NullableLazyValue<String> myUserHomeProvider = lazyNullable(this::readUserHome); private final NullableLazyValue<String> myUserHomeProvider = lazyNullable(this::readUserHome);
@@ -550,34 +581,40 @@ public class WSLDistribution implements AbstractWslDistribution {
return WslConstants.UNC_PREFIX + myDescriptor.getMsId(); 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 // https://docs.microsoft.com/en-us/windows/wsl/compare-versions#accessing-windows-networking-apps-from-linux-host-ip
public String getHostIp() { private @NotNull String readHostIp() throws ExecutionException {
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() {
String wsl1LoopbackAddress = getWsl1LoopbackAddress(); String wsl1LoopbackAddress = getWsl1LoopbackAddress();
if (wsl1LoopbackAddress != null) { if (wsl1LoopbackAddress != null) {
return wsl1LoopbackAddress; return wsl1LoopbackAddress;
} }
if (Registry.is("wsl.obtain.windows.host.ip.alternatively", true)) { 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. // 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. // This transfers the socket into "connected" state including setting the local endpoint according to the system's routing table.
// Works on Windows and Linux. // Works on Windows and Linux.
try (DatagramSocket datagramSocket = new DatagramSocket()) { 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 // 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". // "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). // 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(); return datagramSocket.getLocalAddress().getHostAddress();
} }
catch (Exception e) { 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; return executeAndParseOutput(IdeBundle.message("wsl.win.ip"), strings -> {
try { for (String line : strings) {
output = executeOnWsl(List.of("cat", releaseInfo), new WSLCommandLineOptions(), 10_000, null); if (line.startsWith("nameserver")) {
} return line.substring("nameserver".length()).trim();
catch (ExecutionException e) { }
LOG.info("Cannot read host ip", e); }
return null; return null;
} }, "cat", "/etc/resolv.conf");
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;
} }
private @Nullable String readWslIp() { private @NotNull String readWslIp() throws ExecutionException {
String wsl1LoopbackAddress = getWsl1LoopbackAddress(); String wsl1LoopbackAddress = getWsl1LoopbackAddress();
if (wsl1LoopbackAddress != null) { if (wsl1LoopbackAddress != null) {
return wsl1LoopbackAddress; return wsl1LoopbackAddress;
} }
final ProcessOutput output;
try { return executeAndParseOutput(IdeBundle.message("wsl.wsl.ip"), strings -> {
output = executeOnWsl(List.of("ip", "addr", "show", "eth0"), new WSLCommandLineOptions(), 10_000, null); for (String line : strings) {
} String trimmed = line.trim();
catch (ExecutionException e) { if (trimmed.startsWith("inet ")) {
LOG.info("Cannot read wsl ip", e); int index = trimmed.indexOf("/");
return null; if (index != -1) {
} return trimmed.substring("inet ".length(), index);
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 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<List<@NlsSafe String>, @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() { private @Nullable String getWsl1LoopbackAddress() {
@@ -657,5 +709,4 @@ public class WSLDistribution implements AbstractWslDistribution {
return WslExecution.executeInShellAndGetCommandOnlyStdout(this, new GeneralCommandLine("printenv", "SHELL"), options, DEFAULT_TIMEOUT, return WslExecution.executeInShellAndGetCommandOnlyStdout(this, new GeneralCommandLine("printenv", "SHELL"), options, DEFAULT_TIMEOUT,
true); true);
} }
} }

View File

@@ -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."))
}
}
}
}

View File

@@ -107,7 +107,7 @@ class GradleOnWslExecutionAware : GradleExecutionAware {
} }
override val pathMapper = getTargetPathMapper(wslDistribution) override val pathMapper = getTargetPathMapper(wslDistribution)
override fun getServerBindingAddress(targetEnvironmentConfiguration: TargetEnvironmentConfiguration): HostPort { override fun getServerBindingAddress(targetEnvironmentConfiguration: TargetEnvironmentConfiguration): HostPort {
return HostPort(wslDistribution.wslIp, 0) return HostPort(wslDistribution.wslIpAddress.hostAddress, 0)
} }
} }
} }

View File

@@ -6,7 +6,6 @@ import com.intellij.execution.configurations.RunProfileState
import com.intellij.execution.wsl.WSLDistribution import com.intellij.execution.wsl.WSLDistribution
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk 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.AbstractMavenServerRemoteProcessSupport
import org.jetbrains.idea.maven.server.WslMavenDistribution 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) return WslMavenCmdState(myWslDistribution, myJdk, myOptions, myDistribution as WslMavenDistribution, myDebugPort, myProject, remoteHost)
} }
override fun getRemoteHost(): String { override fun getRemoteHost(): String = myWslDistribution.wslIpAddress.hostAddress
val ip = myWslDistribution.wslIp
if (ip == null) {
throw RuntimeException(SyncBundle.message("maven.sync.wsl.ip.cannot.resolve"))
}
return ip
}
override fun type() = "WSL" override fun type() = "WSL"
} }

View File

@@ -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.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.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.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 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