wsl: properly cache case-insensitive msId, add test

Distributions produced by `wsl.exe -l` should be preferred, thus msIds of different cases should be evicted.

GitOrigin-RevId: 705162e41e54c3c1911f394b2577b4e144c4ca14
This commit is contained in:
Sergey Simonchik
2021-01-06 19:42:00 +00:00
committed by intellij-monorepo-bot
parent c42632cc78
commit cb7908f3a2
5 changed files with 160 additions and 86 deletions
@@ -17,6 +17,7 @@ import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.NullableLazyValue;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.util.text.Strings;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.impl.local.LocalFileSystemBase;
@@ -488,12 +489,12 @@ public class WSLDistribution {
@Override
public boolean equals(Object o) {
return this == o || o != null && getClass() == o.getClass() && myDescriptor.equals(((WSLDistribution)o).myDescriptor);
return this == o || o != null && getClass() == o.getClass() && getMsId().equals(((WSLDistribution)o).getMsId());
}
@Override
public int hashCode() {
return myDescriptor.hashCode();
return Strings.stringHashCodeInsensitive(getMsId());
}
/** @deprecated use {@link WSLDistribution#getUNCRootPath()} instead */
@@ -1,39 +1,27 @@
// 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.wsl;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.process.ProcessOutput;
import com.intellij.execution.util.ExecUtil;
import com.intellij.ide.SaveAndSyncHandler;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.Service;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.TimeoutUtil;
import com.intellij.util.concurrency.AppExecutorUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.text.CaseInsensitiveStringHashingStrategy;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@Service(Service.Level.APP)
public final class WslDistributionManager implements Disposable {
public abstract class WslDistributionManager implements Disposable {
private static final Logger LOG = Logger.getInstance(WslDistributionManager.class);
// Distributions created by tools, e.g. Docker. Not suitable for running users apps.
private static final Set<String> INTERNAL_DISTRIBUTIONS = Set.of("docker-desktop-data");
static final Logger LOG = Logger.getInstance(WslDistributionManager.class);
private static final Object LOCK = new Object();
public static @NotNull WslDistributionManager getInstance() {
@@ -41,7 +29,8 @@ public final class WslDistributionManager implements Disposable {
}
private volatile CachedDistributions myInstalledDistributions;
private final Map<String, WSLDistribution> myMsIdToDistributionCache = ContainerUtil.createConcurrentWeakMap();
private final Map<String, WSLDistribution> myMsIdToDistributionCache = ContainerUtil.createConcurrentWeakMap(
CaseInsensitiveStringHashingStrategy.INSTANCE);
@Override
public void dispose() {
@@ -80,24 +69,27 @@ public final class WslDistributionManager implements Disposable {
/**
* @return {@link WSLDistribution} instance by WSL distribution name. Please note that
* the returned distribution is not guaranteed to be installed actually (for that check if the distribution is contained in
* the returned distribution is not guaranteed to be installed (for that, check if the distribution is contained in
* {@link #getInstalledDistributions}).
* @param msId WSL distribution name, same as produced by `wsl.exe -l`
*/
public @NotNull WSLDistribution getOrCreateDistributionByMsId(@NonNls @NotNull String msId) {
return getOrCreateDistributionByMsId(msId, false);
}
private @NotNull WSLDistribution getOrCreateDistributionByMsId(@NonNls @NotNull String msId, boolean overrideCaseInsensitively) {
if (msId.isEmpty()) {
throw new IllegalStateException("WSL msId is empty");
}
// reuse previously created WSLDistribution instances to avoid re-calculating Host IP / WSL IP
WSLDistribution d = myMsIdToDistributionCache.get(msId);
if (d != null) {
return d;
}
synchronized (myMsIdToDistributionCache) {
d = myMsIdToDistributionCache.get(msId);
if (d == null) {
d = new WSLDistribution(msId);
myMsIdToDistributionCache.put(msId, d);
if (d == null || (overrideCaseInsensitively && !d.getMsId().equals(msId))) {
synchronized (myMsIdToDistributionCache) {
d = myMsIdToDistributionCache.get(msId);
if (d == null || (overrideCaseInsensitively && !d.getMsId().equals(msId))) {
d = new WSLDistribution(msId);
myMsIdToDistributionCache.put(msId, d);
}
}
}
return d;
@@ -116,7 +108,7 @@ public final class WslDistributionManager implements Disposable {
String distName = path.substring(0, index);
String wslPath = FileUtil.toSystemIndependentName(path.substring(index));
return Pair.create(wslPath, getOrCreateDistributionByMsId(distName));
return Pair.create(wslPath, getOrCreateDistributionByMsId(distName, false));
}
@Nullable
@@ -133,65 +125,12 @@ public final class WslDistributionManager implements Disposable {
}
private @NotNull List<WSLDistribution> loadInstalledDistributions() {
checkEdtAndReadAction();
if (!new WSLCommandLineOptions().isLaunchWithWslExe()) {
return Collections.emptyList();
}
try {
long startNano = System.nanoTime();
Pair<GeneralCommandLine, List<WSLDistribution>> result = doFetchDistributionsFromWslCli();
LOG.info("Fetched WSL distributions: " + ContainerUtil.map(result.second, WSLDistribution::getMsId) +
" (\"" + result.first.getCommandLineString() + "\" done in " + TimeoutUtil.getDurationMillis(startNano) + " ms)");
return result.second;
}
catch (IOException e) {
LOG.info("Cannot parse WSL distributions", e);
return Collections.emptyList();
}
}
private @NotNull Pair<GeneralCommandLine, List<WSLDistribution>> doFetchDistributionsFromWslCli() throws IOException {
GeneralCommandLine commandLine = createCommandLine();
ProcessOutput output;
try {
output = ExecUtil.execAndGetOutput(commandLine, 10_000);
}
catch (ExecutionException e) {
throw new IOException("Failed to run " + commandLine.getCommandLineString(), e);
}
if (output.isTimeout() || output.getExitCode() != 0 || !output.getStderr().isEmpty()) {
String details = StringUtil.join(ContainerUtil.newArrayList(
"timeout: " + output.isTimeout(),
"exitCode: " + output.getExitCode(),
"stdout: " + output.getStdout(),
"stderr: " + output.getStderr()
), ", ");
throw new IOException("Failed to run " + commandLine.getCommandLineString() + ": " + details);
}
List<@NlsSafe String> msIds = ContainerUtil.filter(output.getStdoutLines(), distribution -> {
return !INTERNAL_DISTRIBUTIONS.contains(distribution);
return ContainerUtil.map(loadInstalledDistributionMsIds(), (msId) -> {
return getOrCreateDistributionByMsId(msId, true);
});
return Pair.create(commandLine, ContainerUtil.map(msIds, this::getOrCreateDistributionByMsId));
}
private static @NotNull GeneralCommandLine createCommandLine() throws IOException {
Path wslExe = WSLDistribution.findWslExe();
if (wslExe == null) {
throw new IOException("No wsl.exe found in %PATH%");
}
return new GeneralCommandLine(wslExe.toString(), "--list", "--quiet").withCharset(StandardCharsets.UTF_16LE);
}
private static void checkEdtAndReadAction() {
Application application = ApplicationManager.getApplication();
if (application == null || !application.isInternal() || application.isHeadlessEnvironment()) {
return;
}
if (application.isReadAccessAllowed()) {
LOG.error("Please call WslDistributionManager.getInstalledDistributions on a background thread and " +
"not under read action as it runs a potentially long operation.");
}
}
protected abstract @NotNull List<String> loadInstalledDistributionMsIds();
private static class CachedDistributions {
private final @NotNull List<WSLDistribution> myInstalledDistributions;
@@ -0,0 +1,90 @@
// 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.wsl;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.process.ProcessOutput;
import com.intellij.execution.util.ExecUtil;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.TimeoutUtil;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Set;
public final class WslDistributionManagerImpl extends WslDistributionManager {
// Distributions created by tools, e.g. Docker. Not suitable for running users apps.
private static final Set<String> INTERNAL_DISTRIBUTIONS = Set.of("docker-desktop-data");
@Override
protected @NotNull List<String> loadInstalledDistributionMsIds() {
checkEdtAndReadAction();
if (!new WSLCommandLineOptions().isLaunchWithWslExe()) {
return Collections.emptyList();
}
try {
long startNano = System.nanoTime();
Pair<GeneralCommandLine, List<String>> result = doFetchDistributionsFromWslCli();
LOG.info("Fetched WSL distributions: " + result.second +
" (\"" + result.first.getCommandLineString() + "\" done in " + TimeoutUtil.getDurationMillis(startNano) + " ms)");
return result.second;
}
catch (IOException e) {
LOG.info("Cannot parse WSL distributions", e);
return Collections.emptyList();
}
}
private static @NotNull Pair<GeneralCommandLine, List<String>> doFetchDistributionsFromWslCli() throws IOException {
GeneralCommandLine commandLine = createCommandLine();
ProcessOutput output;
try {
output = ExecUtil.execAndGetOutput(commandLine, 10_000);
}
catch (ExecutionException e) {
throw new IOException("Failed to run " + commandLine.getCommandLineString(), e);
}
if (output.isTimeout() || output.getExitCode() != 0 || !output.getStderr().isEmpty()) {
String details = StringUtil.join(ContainerUtil.newArrayList(
"timeout: " + output.isTimeout(),
"exitCode: " + output.getExitCode(),
"stdout: " + output.getStdout(),
"stderr: " + output.getStderr()
), ", ");
throw new IOException("Failed to run " + commandLine.getCommandLineString() + ": " + details);
}
List<@NlsSafe String> msIds = ContainerUtil.filter(output.getStdoutLines(true), distribution -> {
return !INTERNAL_DISTRIBUTIONS.contains(distribution);
});
return Pair.create(commandLine, msIds);
}
private static @NotNull GeneralCommandLine createCommandLine() throws IOException {
Path wslExe = WSLDistribution.findWslExe();
if (wslExe == null) {
throw new IOException("No wsl.exe found in %PATH%");
}
return new GeneralCommandLine(wslExe.toString(), "--list", "--quiet").withCharset(StandardCharsets.UTF_16LE);
}
private static void checkEdtAndReadAction() {
Application application = ApplicationManager.getApplication();
if (application == null || !application.isInternal() || application.isHeadlessEnvironment()) {
return;
}
if (application.isReadAccessAllowed()) {
LOG.error("Please call WslDistributionManager.getInstalledDistributions on a background thread and " +
"not under read action as it runs a potentially long operation.");
}
}
}
@@ -121,6 +121,8 @@
<experimentalFeature id="wsl.execute.with.wsl.exe" percentOfUsers="100">
<description>Run WSL commands using wsl.exe, e.g. "wsl.exe --distribution Ubuntu-20.04" (available since Windows 10 update 1903).</description>
</experimentalFeature>
<applicationService serviceInterface="com.intellij.execution.wsl.WslDistributionManager"
serviceImplementation="com.intellij.execution.wsl.WslDistributionManagerImpl"/>
<executionTargetType implementation="com.intellij.execution.wsl.target.WslTargetType"/>
<virtualFileSystem implementationClass="com.intellij.openapi.vfs.impl.local.LocalFileSystemImpl" key="file" physical="true"/>
@@ -0,0 +1,42 @@
// Copyright 2000-2021 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.wsl;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.testFramework.ServiceContainerUtil;
import com.intellij.testFramework.fixtures.BareTestFixtureTestCase;
import org.jetbrains.annotations.NotNull;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
public class WslDistributionManagerTest extends BareTestFixtureTestCase {
@Test
public void caseInsensitiveDistributionName() {
String ubuntuName = "Ubuntu";
String lowerCaseUbuntuName = StringUtil.toLowerCase(ubuntuName);
String debianName = "Debian";
ServiceContainerUtil.replaceService(
ApplicationManager.getApplication(), WslDistributionManager.class,
new WslDistributionManager() {
@Override
protected @NotNull List<String> loadInstalledDistributionMsIds() {
return List.of(ubuntuName, debianName);
}
}, getTestRootDisposable());
WslDistributionManager distributionManager = WslDistributionManager.getInstance();
WSLDistribution lowerCaseUbuntu = distributionManager.getOrCreateDistributionByMsId(lowerCaseUbuntuName);
assertEquals(lowerCaseUbuntuName, lowerCaseUbuntu.getMsId());
WSLDistribution debian = distributionManager.getOrCreateDistributionByMsId(debianName);
assertEquals(debianName, debian.getMsId());
// Load the installed distributions to replace previously created distributions with different case.
distributionManager.getInstalledDistributions();
assertEquals(ubuntuName, distributionManager.getOrCreateDistributionByMsId(lowerCaseUbuntuName).getMsId());
assertEquals(debianName, distributionManager.getOrCreateDistributionByMsId(debianName).getMsId());
}
}