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 57e5c5bf5c19..f1fba9405ab3 100644 --- a/platform/platform-impl/src/com/intellij/execution/wsl/WSLDistribution.java +++ b/platform/platform-impl/src/com/intellij/execution/wsl/WSLDistribution.java @@ -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 */ diff --git a/platform/platform-impl/src/com/intellij/execution/wsl/WslDistributionManager.java b/platform/platform-impl/src/com/intellij/execution/wsl/WslDistributionManager.java index 01fd86fc6a8b..e85f3b652be7 100644 --- a/platform/platform-impl/src/com/intellij/execution/wsl/WslDistributionManager.java +++ b/platform/platform-impl/src/com/intellij/execution/wsl/WslDistributionManager.java @@ -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 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 myMsIdToDistributionCache = ContainerUtil.createConcurrentWeakMap(); + private final Map 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 loadInstalledDistributions() { - checkEdtAndReadAction(); - if (!new WSLCommandLineOptions().isLaunchWithWslExe()) { - return Collections.emptyList(); - } - try { - long startNano = System.nanoTime(); - Pair> 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> 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 loadInstalledDistributionMsIds(); private static class CachedDistributions { private final @NotNull List myInstalledDistributions; diff --git a/platform/platform-impl/src/com/intellij/execution/wsl/WslDistributionManagerImpl.java b/platform/platform-impl/src/com/intellij/execution/wsl/WslDistributionManagerImpl.java new file mode 100644 index 000000000000..9ec49d6e56e0 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/execution/wsl/WslDistributionManagerImpl.java @@ -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 INTERNAL_DISTRIBUTIONS = Set.of("docker-desktop-data"); + + @Override + protected @NotNull List loadInstalledDistributionMsIds() { + checkEdtAndReadAction(); + if (!new WSLCommandLineOptions().isLaunchWithWslExe()) { + return Collections.emptyList(); + } + try { + long startNano = System.nanoTime(); + Pair> 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> 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."); + } + } +} diff --git a/platform/platform-resources/src/META-INF/PlatformExtensions.xml b/platform/platform-resources/src/META-INF/PlatformExtensions.xml index 2caab1472065..0d1a006c1488 100644 --- a/platform/platform-resources/src/META-INF/PlatformExtensions.xml +++ b/platform/platform-resources/src/META-INF/PlatformExtensions.xml @@ -121,6 +121,8 @@ Run WSL commands using wsl.exe, e.g. "wsl.exe --distribution Ubuntu-20.04" (available since Windows 10 update 1903). + diff --git a/platform/platform-tests/testSrc/com/intellij/execution/wsl/WslDistributionManagerTest.java b/platform/platform-tests/testSrc/com/intellij/execution/wsl/WslDistributionManagerTest.java new file mode 100644 index 000000000000..fc37404db0bb --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/execution/wsl/WslDistributionManagerTest.java @@ -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 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()); + } +}