show local and WSL roots in FileChooser dialog immediately; update WSL roots later...
[idea/community.git] / platform / platform-impl / src / com / intellij / execution / wsl / WslDistributionManager.java
1 // 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.
2 package com.intellij.execution.wsl;
3
4 import com.intellij.ide.SaveAndSyncHandler;
5 import com.intellij.openapi.Disposable;
6 import com.intellij.openapi.application.ApplicationManager;
7 import com.intellij.openapi.diagnostic.Logger;
8 import com.intellij.openapi.util.io.FileUtilRt;
9 import com.intellij.openapi.util.registry.Registry;
10 import com.intellij.openapi.vfs.impl.wsl.WslConstants;
11 import com.intellij.util.concurrency.AppExecutorUtil;
12 import com.intellij.util.containers.CollectionFactory;
13 import com.intellij.util.containers.ContainerUtil;
14 import org.jetbrains.annotations.NonNls;
15 import org.jetbrains.annotations.NotNull;
16 import org.jetbrains.annotations.Nullable;
17
18 import java.io.IOException;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.concurrent.CompletableFuture;
22
23 public abstract class WslDistributionManager implements Disposable {
24   static final Logger LOG = Logger.getInstance(WslDistributionManager.class);
25   private static final Object LOCK = new Object();
26
27   public static @NotNull WslDistributionManager getInstance() {
28     return ApplicationManager.getApplication().getService(WslDistributionManager.class);
29   }
30
31   private volatile CachedDistributions myInstalledDistributions;
32   private volatile List<WSLDistribution> myLastInstalledDistributions;
33   private final Map<String, WSLDistribution> myMsIdToDistributionCache = CollectionFactory.createConcurrentWeakCaseInsensitiveMap();
34
35   @Override
36   public void dispose() {
37     myMsIdToDistributionCache.clear();
38     myInstalledDistributions = null;
39   }
40
41   /**
42    * @return not-null if installed distribution list is up-to-date; otherwise, return null and initialize update in background.
43    */
44   public @Nullable List<WSLDistribution> getCachedInstalledDistributions() {
45     return getInstalledDistributionsFuture().getNow(null);
46   }
47
48   /**
49    * @return last loaded list of installed distributions or {@code null} if it hasn't been loaded yet.
50    * Please note the returned list might be out-of-date. To get the up-to-date list, please use {@link #getInstalledDistributionsFuture}.
51    */
52   public @Nullable List<WSLDistribution> getLastInstalledDistributions() {
53     return myLastInstalledDistributions;
54   }
55
56   /**
57    * @return list of installed WSL distributions by parsing output of `wsl.exe -l`. Please call it
58    * on a pooled thread and outside of the read action as it runs a process under the hood.
59    * @see #getInstalledDistributionsFuture
60    */
61   public @NotNull List<WSLDistribution> getInstalledDistributions() {
62     CachedDistributions cachedDistributions = myInstalledDistributions;
63     if (cachedDistributions != null && cachedDistributions.isUpToDate()) {
64       return cachedDistributions.myInstalledDistributions;
65     }
66     myInstalledDistributions = null;
67     synchronized (LOCK) {
68       cachedDistributions = myInstalledDistributions;
69       if (cachedDistributions == null) {
70         cachedDistributions = new CachedDistributions(loadInstalledDistributions());
71         myInstalledDistributions = cachedDistributions;
72         myLastInstalledDistributions = cachedDistributions.myInstalledDistributions;
73       }
74     }
75     return cachedDistributions.myInstalledDistributions;
76   }
77
78   public @NotNull CompletableFuture<List<WSLDistribution>> getInstalledDistributionsFuture() {
79     CachedDistributions cachedDistributions = myInstalledDistributions;
80     if (cachedDistributions != null && cachedDistributions.isUpToDate()) {
81       return CompletableFuture.completedFuture(cachedDistributions.myInstalledDistributions);
82     }
83     return CompletableFuture.supplyAsync(this::getInstalledDistributions, AppExecutorUtil.getAppExecutorService());
84   }
85
86   /**
87    * @return {@link WSLDistribution} instance by WSL distribution name. Please note that
88    * the returned distribution is not guaranteed to be installed (for that, check if the distribution is contained in
89    * {@link #getInstalledDistributions}).
90    * @param msId WSL distribution name, same as produced by `wsl.exe -l`
91    */
92   public @NotNull WSLDistribution getOrCreateDistributionByMsId(@NonNls @NotNull String msId) {
93     return getOrCreateDistributionByMsId(msId, false);
94   }
95
96   private @NotNull WSLDistribution getOrCreateDistributionByMsId(@NonNls @NotNull String msId, boolean overrideCaseInsensitively) {
97     if (msId.isEmpty()) {
98       throw new IllegalArgumentException("WSL msId is empty");
99     }
100     // reuse previously created WSLDistribution instances to avoid re-calculating Host IP / WSL IP
101     WSLDistribution d = myMsIdToDistributionCache.get(msId);
102     if (d == null || (overrideCaseInsensitively && !d.getMsId().equals(msId))) {
103       synchronized (myMsIdToDistributionCache) {
104         d = myMsIdToDistributionCache.get(msId);
105         if (d == null || (overrideCaseInsensitively && !d.getMsId().equals(msId))) {
106           d = new WSLDistribution(msId);
107           myMsIdToDistributionCache.put(msId, d);
108         }
109       }
110     }
111     return d;
112   }
113
114   public static boolean isWslPath(@NotNull String path) {
115     return FileUtilRt.toSystemDependentName(path).startsWith(WslConstants.UNC_PREFIX);
116   }
117
118   private @NotNull List<WSLDistribution> loadInstalledDistributions() {
119     final int releaseId = WSLUtil.getWindowsReleaseId();
120     if (releaseId > 0 && releaseId < 2004) {
121       final WSLUtil.WSLToolFlags wslTool = WSLUtil.getWSLToolFlags();
122       if (wslTool == null || (!wslTool.isVerboseFlagAvailable && !wslTool.isQuietFlagAvailable)) {
123         return WSLUtil.getAvailableDistributions();
124       }
125     }
126
127     // we assume that after "2004" Windows release wsl.exe and all required flags are available
128     if (Registry.is("wsl.list.prefer.verbose.output", true)) {
129       try {
130         final var result = loadInstalledDistributionsWithVersions();
131         return ContainerUtil.map(result, data -> {
132           final WSLDistribution distribution = getOrCreateDistributionByMsId(data.getDistributionName(), true);
133           distribution.setVersion(data.getVersion());
134           return distribution;
135         });
136       }
137       catch (IOException e) {
138         LOG.warn(e);
139       }
140       catch (IllegalStateException e) {
141         LOG.error(e);
142       }
143     }
144     // fallback: using loadInstalledDistributionMsIds in case of execution exception or parsing error
145     return ContainerUtil.map(loadInstalledDistributionMsIds(), (msId) -> {
146       return getOrCreateDistributionByMsId(msId, true);
147     });
148   }
149
150   protected abstract @NotNull List<String> loadInstalledDistributionMsIds();
151
152   /**
153    * @throws IOException if an execution error occurs
154    * @throws IllegalStateException if a parsing error occurs
155    */
156   public abstract @NotNull List<WslDistributionAndVersion> loadInstalledDistributionsWithVersions()
157     throws IOException, IllegalStateException;
158
159   private static class CachedDistributions {
160     private final @NotNull List<WSLDistribution> myInstalledDistributions;
161     private final long myExternalChangesCount;
162
163     private CachedDistributions(@NotNull List<WSLDistribution> installedDistributions) {
164       myInstalledDistributions = List.copyOf(installedDistributions);
165       myExternalChangesCount = getCurrentExternalChangesCount();
166     }
167
168     public boolean isUpToDate() {
169       return getCurrentExternalChangesCount() == myExternalChangesCount;
170     }
171
172     private static long getCurrentExternalChangesCount() {
173       return SaveAndSyncHandler.getInstance().getExternalChangesTracker().getModificationCount();
174     }
175   }
176 }