0e1dc01805de9a8d53ddd1f8397c39774ff8d443
[idea/community.git] / platform / platform-impl / src / com / intellij / execution / wsl / WSLDistribution.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.google.common.net.InetAddresses;
5 import com.intellij.credentialStore.CredentialAttributes;
6 import com.intellij.credentialStore.CredentialPromptDialog;
7 import com.intellij.execution.CommandLineUtil;
8 import com.intellij.execution.ExecutionException;
9 import com.intellij.execution.configurations.GeneralCommandLine;
10 import com.intellij.execution.configurations.PathEnvironmentVariableUtil;
11 import com.intellij.execution.process.*;
12 import com.intellij.ide.IdeBundle;
13 import com.intellij.openapi.application.Application;
14 import com.intellij.openapi.application.ApplicationManager;
15 import com.intellij.openapi.application.Experiments;
16 import com.intellij.openapi.project.Project;
17 import com.intellij.openapi.util.AtomicNotNullLazyValue;
18 import com.intellij.openapi.util.Key;
19 import com.intellij.openapi.util.NlsSafe;
20 import com.intellij.openapi.util.NullableLazyValue;
21 import com.intellij.openapi.util.io.FileUtil;
22 import com.intellij.openapi.util.registry.Registry;
23 import com.intellij.openapi.util.text.StringUtil;
24 import com.intellij.openapi.util.text.Strings;
25 import com.intellij.openapi.vfs.VfsUtil;
26 import com.intellij.openapi.vfs.VirtualFile;
27 import com.intellij.openapi.vfs.impl.local.LocalFileSystemBase;
28 import com.intellij.util.*;
29 import com.intellij.util.containers.ContainerUtil;
30 import org.jetbrains.annotations.ApiStatus;
31 import org.jetbrains.annotations.NonNls;
32 import org.jetbrains.annotations.NotNull;
33 import org.jetbrains.annotations.Nullable;
34
35 import java.io.File;
36 import java.io.OutputStream;
37 import java.io.PrintWriter;
38 import java.net.DatagramSocket;
39 import java.net.InetAddress;
40 import java.nio.file.Path;
41 import java.nio.file.Paths;
42 import java.util.*;
43
44 import static com.intellij.execution.wsl.WSLUtil.LOG;
45
46 /**
47  * 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>
48  *
49  * @see WSLUtil
50  */
51 public class WSLDistribution {
52   public static final String DEFAULT_WSL_MNT_ROOT = "/mnt/";
53   private static final int RESOLVE_SYMLINK_TIMEOUT = 10000;
54   private static final String RUN_PARAMETER = "run";
55   public static final String UNC_PREFIX = "\\\\wsl$\\";
56   private static final String WSLENV = "WSLENV";
57   private static final int DEFAULT_TIMEOUT = SystemProperties.getIntProperty("ide.wsl.probe.timeout", 20_000);
58
59   private static final Key<ProcessListener> SUDO_LISTENER_KEY = Key.create("WSL sudo listener");
60
61   private final @NotNull WslDistributionDescriptor myDescriptor;
62   private final @Nullable Path myExecutablePath;
63   private final NullableLazyValue<String> myHostIp = NullableLazyValue.createValue(this::readHostIp);
64   private final NullableLazyValue<String> myWslIp = NullableLazyValue.createValue(this::readWslIp);
65   private final NullableLazyValue<String> myShellPath = NullableLazyValue.createValue(this::readShellPath);
66   private final NullableLazyValue<String> myUserHomeProvider = NullableLazyValue.createValue(this::readUserHome);
67   private final @NotNull AtomicNotNullLazyValue<Boolean> isWSL1 =
68     AtomicNotNullLazyValue.createValue(() -> WSLUtil.isWsl1(this) != ThreeState.NO);
69
70   protected WSLDistribution(@NotNull WSLDistribution dist) {
71     this(dist.myDescriptor, dist.myExecutablePath);
72   }
73
74   WSLDistribution(@NotNull WslDistributionDescriptor descriptor, @Nullable Path executablePath) {
75     myDescriptor = descriptor;
76     myExecutablePath = executablePath;
77   }
78
79   public WSLDistribution(@NotNull String msId) {
80     this(new WslDistributionDescriptor(msId), null);
81   }
82
83   /**
84    * @deprecated please don't use it, to be removed
85    * @return executable file, null for WSL distributions parsed from `wsl.exe --list` output
86    */
87   @ApiStatus.ScheduledForRemoval(inVersion = "2021.3")
88   @Deprecated
89   public @Nullable Path getExecutablePath() {
90     return myExecutablePath;
91   }
92
93   /**
94    * @return identification data of WSL distribution.
95    */
96   public @Nullable @NlsSafe String readReleaseInfo() {
97     try {
98       final String key = "PRETTY_NAME";
99       final String releaseInfo = "/etc/os-release"; // available for all distributions
100       final ProcessOutput output = executeOnWsl(10000, "cat", releaseInfo);
101       if (LOG.isDebugEnabled()) LOG.debug("Reading release info: " + getId());
102       if (!output.checkSuccess(LOG)) return null;
103       for (String line : output.getStdoutLines(true)) {
104         if (line.startsWith(key) && line.length() >= (key.length() + 1)) {
105           final String prettyName = line.substring(key.length() + 1);
106           return StringUtil.nullize(StringUtil.unquoteString(prettyName));
107         }
108       }
109     }
110     catch (ExecutionException e) {
111       LOG.warn(e);
112     }
113     return null;
114   }
115
116   public boolean isWSL1() {
117     return isWSL1.getValue();
118   }
119
120   /**
121    * @return creates and patches command line, e.g:
122    * {@code ruby -v} => {@code bash -c "ruby -v"}
123    */
124   public @NotNull GeneralCommandLine createWslCommandLine(String @NotNull ... command) throws ExecutionException {
125     return patchCommandLine(new GeneralCommandLine(command), null, new WSLCommandLineOptions());
126   }
127
128   /**
129    * Creates a patched command line, executes it on wsl distribution and returns output
130    *
131    * @param command                linux command, eg {@code gem env}
132    * @param options                {@link WSLCommandLineOptions} instance
133    * @param timeout                timeout in ms
134    * @param processHandlerConsumer consumes process handler just before execution, may be used for cancellation
135    */
136   public @NotNull ProcessOutput executeOnWsl(@NotNull List<String> command,
137                                              @NotNull WSLCommandLineOptions options,
138                                              int timeout,
139                                              @Nullable Consumer<? super ProcessHandler> processHandlerConsumer) throws ExecutionException {
140     GeneralCommandLine commandLine = patchCommandLine(new GeneralCommandLine(command), null, options);
141     CapturingProcessHandler processHandler = new CapturingProcessHandler(commandLine);
142     if (processHandlerConsumer != null) {
143       processHandlerConsumer.consume(processHandler);
144     }
145     return processHandler.runProcess(timeout);
146   }
147
148   private @NotNull ProcessOutput executeOnWsl(@NotNull GeneralCommandLine commandLine,
149                                               @NotNull WSLCommandLineOptions options,
150                                               int timeout) throws ExecutionException {
151     patchCommandLine(commandLine, null, options);
152     CapturingProcessHandler processHandler = new CapturingProcessHandler(commandLine);
153     return processHandler.runProcess(timeout);
154   }
155
156   public @NotNull ProcessOutput executeOnWsl(int timeout, @NonNls String @NotNull ... command) throws ExecutionException {
157     return executeOnWsl(Arrays.asList(command), new WSLCommandLineOptions(), timeout, null);
158   }
159
160   /**
161    * Copying changed files recursively from wslPath/ to windowsPath/; with rsync
162    *
163    * @param wslPath           source path inside wsl, e.g. /usr/bin
164    * @param windowsPath       target windows path, e.g. C:/tmp; Directory going to be created
165    * @param additionalOptions may be used for --delete (not recommended), --include and so on
166    * @param handlerConsumer   consumes process handler just before execution. Can be used for fast cancellation
167    * @return process output
168    */
169
170   @SuppressWarnings("UnusedReturnValue")
171   public ProcessOutput copyFromWsl(@NotNull String wslPath,
172                                    @NotNull String windowsPath,
173                                    @Nullable List<String> additionalOptions,
174                                    @Nullable Consumer<? super ProcessHandler> handlerConsumer
175   )
176     throws ExecutionException {
177     //noinspection ResultOfMethodCallIgnored
178     new File(windowsPath).mkdirs();
179     List<String> command = new ArrayList<>(Arrays.asList("rsync", "-cr"));
180
181     if (additionalOptions != null) {
182       command.addAll(additionalOptions);
183     }
184
185     command.add(wslPath + "/");
186     String targetWslPath = getWslPath(windowsPath);
187     if (targetWslPath == null) {
188       throw new ExecutionException(IdeBundle.message("wsl.rsync.unable.to.copy.files.dialog.message", windowsPath));
189     }
190     command.add(targetWslPath + "/");
191     return executeOnWsl(command, new WSLCommandLineOptions(), -1, handlerConsumer);
192   }
193
194   /**
195    * @deprecated use {@link #patchCommandLine(GeneralCommandLine, Project, WSLCommandLineOptions)} instead
196    */
197   @Deprecated
198   @ApiStatus.ScheduledForRemoval(inVersion = "2021.3")
199   public @NotNull <T extends GeneralCommandLine> T patchCommandLine(@NotNull T commandLine,
200                                                                     @Nullable Project project,
201                                                                     @Nullable String remoteWorkingDir,
202                                                                     boolean askForSudo) {
203     WSLCommandLineOptions options = new WSLCommandLineOptions()
204       .setRemoteWorkingDirectory(remoteWorkingDir)
205       .setSudo(askForSudo);
206     try {
207       return patchCommandLine(commandLine, project, options);
208     }
209     catch (ExecutionException e) {
210       throw new IllegalStateException("Cannot patch command line for WSL", e);
211     }
212   }
213
214   /**
215    * Patches passed command line to make it runnable in WSL context, e.g changes {@code date} to {@code ubuntu run "date"}.<p/>
216    * <p>
217    * Environment variables and working directory are mapped to the chain calls: working dir using {@code cd} and environment variables using {@code export},
218    * e.g {@code bash -c "export var1=val1 && export var2=val2 && cd /some/working/dir && date"}.<p/>
219    * <p>
220    * Method should properly handle quotation and escaping of the environment variables.<p/>
221    *
222    * @param commandLine command line to patch
223    * @param project     current project
224    * @param options     {@link WSLCommandLineOptions} instance
225    * @param <T>         GeneralCommandLine or descendant
226    * @return original {@code commandLine}, prepared to run in WSL context
227    */
228   public @NotNull <T extends GeneralCommandLine> T patchCommandLine(@NotNull T commandLine,
229                                                                     @Nullable Project project,
230                                                                     @NotNull WSLCommandLineOptions options) throws ExecutionException {
231     logCommandLineBefore(commandLine, options);
232     Path executable = getExecutablePath();
233     boolean launchWithWslExe = options.isLaunchWithWslExe() || executable == null;
234     Path wslExe = launchWithWslExe ? findWslExe() : null;
235     if (wslExe == null && executable == null) {
236       throw new ExecutionException(IdeBundle.message("wsl.not.installed.dialog.message"));
237     }
238     boolean executeCommandInShell = wslExe == null || options.isExecuteCommandInShell();
239     List<String> linuxCommand = buildLinuxCommand(commandLine, executeCommandInShell);
240
241     final boolean isElevated = options.isSudo();
242     // use old approach in case of wsl.exe is not available
243     if (isElevated && wslExe == null) { // fixme shouldn't we sudo for every chunk? also, preserve-env, login?
244       prependCommand(linuxCommand, "sudo", "-S", "-p", "''");
245       //TODO[traff]: ask password only if it is needed. When user is logged as root, password isn't asked.
246
247       SUDO_LISTENER_KEY.set(commandLine, new ProcessAdapter() {
248         @Override
249         public void startNotified(@NotNull ProcessEvent event) {
250           OutputStream input = event.getProcessHandler().getProcessInput();
251           if (input == null) {
252             return;
253           }
254           String password = CredentialPromptDialog.askPassword(
255             project,
256             IdeBundle.message("wsl.enter.root.password.dialog.title"),
257             IdeBundle.message("wsl.sudo.password.for.root.label", getPresentableName()),
258             new CredentialAttributes("WSL", "root", WSLDistribution.class),
259             true
260           );
261           if (password != null) {
262             try (PrintWriter pw = new PrintWriter(input, false, commandLine.getCharset())) {
263               pw.println(password);
264             }
265           }
266           else {
267             // fixme notify user?
268           }
269           super.startNotified(event);
270         }
271       });
272     }
273
274     if (executeCommandInShell && StringUtil.isNotEmpty(options.getRemoteWorkingDirectory())) {
275       prependCommand(linuxCommand, "cd", CommandLineUtil.posixQuote(options.getRemoteWorkingDirectory()), "&&");
276     }
277     if (executeCommandInShell && !options.isPassEnvVarsUsingInterop()) {
278       commandLine.getEnvironment().forEach((key, val) -> {
279         prependCommand(linuxCommand, "export", CommandLineUtil.posixQuote(key) + "=" + CommandLineUtil.posixQuote(val), "&&");
280       });
281       commandLine.getEnvironment().clear();
282     }
283     else {
284       passEnvironmentUsingInterop(commandLine);
285     }
286     if (executeCommandInShell) {
287       for (String command : options.getInitShellCommands()) {
288         prependCommand(linuxCommand, command, "&&");
289       }
290     }
291
292     commandLine.getParametersList().clearAll();
293     String linuxCommandStr = StringUtil.join(linuxCommand, " ");
294     if (wslExe != null) {
295       commandLine.setExePath(wslExe.toString());
296       if (isElevated) {
297         commandLine.addParameters("-u", "root");
298       }
299       commandLine.addParameters("--distribution", getMsId());
300       if (options.isExecuteCommandInShell()) {
301         // workaround WSL1 problem: https://github.com/microsoft/WSL/issues/4082
302         if (options.getSleepTimeoutSec() > 0 && isWSL1()) {
303           linuxCommandStr += " && sleep " + options.getSleepTimeoutSec();
304         }
305
306         if (options.isExecuteCommandInDefaultShell()) {
307           commandLine.addParameters("$SHELL", "-c", linuxCommandStr);
308         }
309         else {
310           commandLine.addParameters("--exec", options.getShellPath());
311           if (options.isExecuteCommandInInteractiveShell()) {
312             commandLine.addParameters("-i");
313           }
314           if (options.isExecuteCommandInLoginShell()) {
315             commandLine.addParameters("-l");
316           }
317           commandLine.addParameters("-c", linuxCommandStr);
318         }
319       }
320       else {
321         commandLine.addParameter("--exec");
322         commandLine.addParameters(linuxCommand);
323       }
324     }
325     else {
326       commandLine.setExePath(executable.toString());
327       commandLine.addParameter(getRunCommandLineParameter());
328       commandLine.addParameter(linuxCommandStr);
329     }
330
331     logCommandLineAfter(commandLine);
332     return commandLine;
333   }
334
335   private void logCommandLineBefore(@NotNull GeneralCommandLine commandLine, @NotNull WSLCommandLineOptions options) {
336     if (LOG.isTraceEnabled()) {
337       LOG.trace("[" + getId() + "] " +
338                 "Patching: " +
339                 commandLine.getCommandLineString() +
340                 "; options: " +
341                 options +
342                 "; envs: " + commandLine.getEnvironment()
343       );
344     }
345   }
346
347   private void logCommandLineAfter(@NotNull GeneralCommandLine commandLine) {
348     if (LOG.isDebugEnabled()) {
349       LOG.debug("[" + getId() + "] " + "Patched as: " + commandLine.getCommandLineList(null));
350     }
351   }
352
353   public static @Nullable Path findWslExe() {
354     File file = PathEnvironmentVariableUtil.findInPath("wsl.exe");
355     return file != null ? file.toPath() : null;
356   }
357
358   private static @NotNull List<String> buildLinuxCommand(@NotNull GeneralCommandLine commandLine, boolean executeCommandInShell) {
359     List<String> command = ContainerUtil.concat(List.of(commandLine.getExePath()), commandLine.getParametersList().getList());
360     return new ArrayList<>(ContainerUtil.map(command, executeCommandInShell ? CommandLineUtil::posixQuote : Functions.identity()));
361   }
362
363   // https://blogs.msdn.microsoft.com/commandline/2017/12/22/share-environment-vars-between-wsl-and-windows/
364   private static void passEnvironmentUsingInterop(@NotNull GeneralCommandLine commandLine) {
365     StringBuilder builder = new StringBuilder();
366     for (String envName : commandLine.getEnvironment().keySet()) {
367       if (StringUtil.isNotEmpty(envName)) {
368         if (builder.length() > 0) {
369           builder.append(":");
370         }
371         builder.append(envName).append("/u");
372       }
373     }
374     if (builder.length() > 0) {
375       String prevValue = commandLine.getEnvironment().get(WSLENV);
376       if (prevValue == null) {
377         prevValue = commandLine.getParentEnvironment().get(WSLENV);
378       }
379       String value = prevValue != null ? StringUtil.trimEnd(prevValue, ':') + ':' + builder
380                                        : builder.toString();
381       commandLine.getEnvironment().put(WSLENV, value);
382     }
383   }
384
385   protected @NotNull @NlsSafe String getRunCommandLineParameter() {
386     return RUN_PARAMETER;
387   }
388
389   /**
390    * Attempts to resolve symlink with a given timeout
391    *
392    * @param path                  path in question
393    * @param timeoutInMilliseconds timeout for execution
394    * @return actual file name
395    */
396   public @NotNull @NlsSafe String resolveSymlink(@NotNull String path, int timeoutInMilliseconds) {
397
398     try {
399       final ProcessOutput output = executeOnWsl(timeoutInMilliseconds, "readlink", "-f", path);
400       if (output.getExitCode() == 0) {
401         String stdout = output.getStdout().trim();
402         if (output.getExitCode() == 0 && StringUtil.isNotEmpty(stdout)) {
403           return stdout;
404         }
405       }
406     }
407     catch (ExecutionException e) {
408       LOG.debug("Error while resolving symlink: " + path, e);
409     }
410     return path;
411   }
412
413   public @NotNull @NlsSafe String resolveSymlink(@NotNull String path) {
414     return resolveSymlink(path, RESOLVE_SYMLINK_TIMEOUT);
415   }
416
417   /**
418    * Patches process handler with sudo listener, asking user for the password
419    *
420    * @param commandLine    patched command line
421    * @param processHandler process handler, created from patched commandline
422    * @return passed processHandler, patched with sudo listener if any
423    */
424   public @NotNull <T extends ProcessHandler> T patchProcessHandler(@NotNull GeneralCommandLine commandLine, @NotNull T processHandler) {
425     ProcessListener listener = SUDO_LISTENER_KEY.get(commandLine);
426     if (listener != null) {
427       processHandler.addProcessListener(listener);
428       SUDO_LISTENER_KEY.set(commandLine, null);
429     }
430     return processHandler;
431   }
432
433   /**
434    * @return environment map of the default user in wsl
435    */
436   public @NotNull Map<String, String> getEnvironment() {
437     try {
438       ProcessOutput processOutput =
439         executeOnWsl(Collections.singletonList("env"),
440                      new WSLCommandLineOptions()
441                        .setExecuteCommandInShell(true)
442                        .setExecuteCommandInLoginShell(true)
443                        .setExecuteCommandInInteractiveShell(true),
444                      5000,
445                      null);
446       Map<String, String> result = new HashMap<>();
447       for (String string : processOutput.getStdoutLines()) {
448         int assignIndex = string.indexOf('=');
449         if (assignIndex == -1) {
450           result.put(string, "");
451         }
452         else {
453           result.put(string.substring(0, assignIndex), string.substring(assignIndex + 1));
454         }
455       }
456       return result;
457     }
458     catch (ExecutionException e) {
459       LOG.warn(e);
460     }
461
462     return Collections.emptyMap();
463   }
464
465   /**
466    * @return Windows-dependent path for a file, pointed by {@code wslPath} in WSL, or {@code null} if path is unmappable
467    */
468
469   public @Nullable @NlsSafe String getWindowsPath(@NotNull String wslPath) {
470     if (wslPath.startsWith(getMntRoot())) {
471       return WSLUtil.getWindowsPath(wslPath, getMntRoot());
472     }
473     return getUNCRoot() + FileUtil.toSystemDependentName(wslPath);
474   }
475
476   /**
477    * @return Linux path for a file pointed by {@code windowsPath} or null if unavailable, like \\MACHINE\path
478    */
479   public @Nullable @NlsSafe String getWslPath(@NotNull String windowsPath) {
480     if (FileUtil.toSystemDependentName(windowsPath).startsWith(UNC_PREFIX)) {
481       windowsPath = StringUtil.trimStart(FileUtil.toSystemDependentName(windowsPath), UNC_PREFIX);
482       int index = windowsPath.indexOf('\\');
483       if (index == -1) return null;
484
485       String distName = windowsPath.substring(0, index);
486       if (!distName.equalsIgnoreCase(myDescriptor.getMsId())) {
487         throw new IllegalArgumentException(
488           "Trying to get WSL path from a different WSL distribution: in path: " + distName + "; mine is: " + myDescriptor.getMsId());
489       }
490       return FileUtil.toSystemIndependentName(windowsPath.substring(index));
491     }
492
493     //noinspection deprecation
494     if (FileUtil.isWindowsAbsolutePath(windowsPath)) { // absolute windows path => /mnt/disk_letter/path
495       return getMntRoot() + convertWindowsPath(windowsPath);
496     }
497     return null;
498   }
499
500   /**
501    * @see WslDistributionDescriptor#getMntRoot()
502    */
503   public final @NotNull @NlsSafe String getMntRoot() {
504     return myDescriptor.getMntRoot();
505   }
506
507   public final @Nullable @NlsSafe String getUserHome() {
508     return myUserHomeProvider.getValue();
509   }
510
511   private @NlsSafe @Nullable String readUserHome() {
512     return getEnvironmentVariable("HOME");
513   }
514
515   /**
516    * @param windowsAbsolutePath properly formatted windows local absolute path: {@code drive:\path}
517    * @return windows path converted to the linux path according to wsl rules: {@code c:\some\path} => {@code c/some/path}
518    */
519   static @NotNull @NlsSafe String convertWindowsPath(@NotNull String windowsAbsolutePath) {
520     return Character.toLowerCase(windowsAbsolutePath.charAt(0)) + FileUtil.toSystemIndependentName(windowsAbsolutePath.substring(2));
521   }
522
523   public @NotNull @NlsSafe String getId() {
524     return myDescriptor.getId();
525   }
526
527   public @NotNull @NlsSafe String getMsId() {
528     return myDescriptor.getMsId();
529   }
530
531   public @NotNull @NlsSafe String getPresentableName() {
532     return myDescriptor.getPresentableName();
533   }
534
535   @Override
536   public String toString() {
537     return "WSLDistribution{myDescriptor=" + myDescriptor + '}';
538   }
539
540   private static void prependCommand(@NotNull List<? super String> command, String @NotNull ... commandToPrepend) {
541     command.addAll(0, Arrays.asList(commandToPrepend));
542   }
543
544   @Override
545   public boolean equals(Object o) {
546     return this == o || o != null && getClass() == o.getClass() && getMsId().equals(((WSLDistribution)o).getMsId());
547   }
548
549   @Override
550   public int hashCode() {
551     return Strings.stringHashCodeInsensitive(getMsId());
552   }
553
554   /** @deprecated use {@link WSLDistribution#getUNCRootPath()} instead */
555   @Deprecated
556   public @NotNull File getUNCRoot() {
557     return new File(UNC_PREFIX + myDescriptor.getMsId());
558   }
559
560   /**
561    * @return UNC root for the distribution, e.g. {@code \\wsl$\Ubuntu}
562    */
563   @ApiStatus.Experimental
564   public @NotNull Path getUNCRootPath() {
565     return Paths.get(UNC_PREFIX + myDescriptor.getMsId());
566   }
567
568   /**
569    * @return UNC root for the distribution, e.g. {@code \\wsl$\Ubuntu}
570    * @implNote there is a hack in {@link LocalFileSystemBase#getAttributes(VirtualFile)} which causes all network
571    * virtual files to exists all the time. So we need to check explicitly that root exists. After implementing proper non-blocking check
572    * for the network resource availability, this method may be simplified to findFileByIoFile
573    * @see VfsUtil#findFileByIoFile(File, boolean)
574    */
575   @ApiStatus.Experimental
576   public @Nullable VirtualFile getUNCRootVirtualFile(boolean refreshIfNeed) {
577     if (!Experiments.getInstance().isFeatureEnabled("wsl.p9.support")) {
578       return null;
579     }
580     File uncRoot = getUNCRoot();
581     return uncRoot.exists() ? VfsUtil.findFileByIoFile(uncRoot, refreshIfNeed) : null;
582   }
583
584   // https://docs.microsoft.com/en-us/windows/wsl/compare-versions#accessing-windows-networking-apps-from-linux-host-ip
585   public String getHostIp() {
586     return myHostIp.getValue();
587   }
588
589   public String getWslIp() {
590     return myWslIp.getValue();
591   }
592
593   public InetAddress getHostIpAddress() {
594     return InetAddresses.forString(getHostIp());
595   }
596
597   public InetAddress getWslIpAddress() {
598     return InetAddresses.forString(getWslIp());
599   }
600
601   private @Nullable String readHostIp() {
602     String wsl1LoopbackAddress = getWsl1LoopbackAddress();
603     if (wsl1LoopbackAddress != null) {
604       return wsl1LoopbackAddress;
605     }
606     if (Registry.is("wsl.obtain.windows.host.ip.alternatively", true)) {
607       InetAddress wslAddr = getWslIpAddress();
608       try (DatagramSocket datagramSocket = new DatagramSocket()) {
609         datagramSocket.connect(wslAddr, 0);
610         return datagramSocket.getLocalAddress().getHostAddress();
611       }
612       catch (Exception e) {
613         LOG.error("Cannot obtain Windows host IP alternatively: failed to connect to WSL IP " + wslAddr + ". Fallback to default way.", e);
614       }
615     }
616     final String releaseInfo = "/etc/resolv.conf"; // available for all distributions
617     final ProcessOutput output;
618     try {
619       output = executeOnWsl(List.of("cat", releaseInfo), new WSLCommandLineOptions(), 10_000, null);
620     }
621     catch (ExecutionException e) {
622       LOG.info("Cannot read host ip", e);
623       return null;
624     }
625     if (LOG.isDebugEnabled()) LOG.debug("Reading release info: " + getId());
626     if (!output.checkSuccess(LOG)) return null;
627     for (String line : output.getStdoutLines(true)) {
628       if (line.startsWith("nameserver")) {
629         return line.substring("nameserver".length()).trim();
630       }
631     }
632     return null;
633   }
634
635   private @Nullable String readWslIp() {
636     String wsl1LoopbackAddress = getWsl1LoopbackAddress();
637     if (wsl1LoopbackAddress != null) {
638       return wsl1LoopbackAddress;
639     }
640     final ProcessOutput output;
641     try {
642       output = executeOnWsl(List.of("ip", "addr", "show", "eth0"), new WSLCommandLineOptions(), 10_000, null);
643     }
644     catch (ExecutionException e) {
645       LOG.info("Cannot read wsl ip", e);
646       return null;
647     }
648     if (LOG.isDebugEnabled()) LOG.debug("Reading eth0 info: " + getId());
649     if (!output.checkSuccess(LOG)) return null;
650     for (String line : output.getStdoutLines(true)) {
651       String trimmed = line.trim();
652       if (trimmed.startsWith("inet ")) {
653         int index = trimmed.indexOf("/");
654         if (index != -1) {
655           return trimmed.substring("inet ".length(), index);
656         }
657       }
658     }
659     return null;
660   }
661
662   private @Nullable String getWsl1LoopbackAddress() {
663     return WSLUtil.isWsl1(this) == ThreeState.YES ? InetAddress.getLoopbackAddress().getHostAddress() : null;
664   }
665
666   public @NonNls @Nullable String getEnvironmentVariable(String name) {
667     WSLCommandLineOptions options = new WSLCommandLineOptions()
668       .setExecuteCommandInInteractiveShell(true)
669       .setExecuteCommandInLoginShell(true)
670       .setShellPath(getShellPath());
671     return executeInShellAndGetCommandOnlyStdout(new GeneralCommandLine("printenv", name), options, DEFAULT_TIMEOUT, true);
672   }
673
674   public @NlsSafe @NotNull String getShellPath() {
675     return ObjectUtils.notNull(myShellPath.getValue(), WSLCommandLineOptions.DEFAULT_SHELL);
676   }
677
678   private @NlsSafe @Nullable String readShellPath() {
679     WSLCommandLineOptions options = new WSLCommandLineOptions().setExecuteCommandInDefaultShell(true);
680     return executeInShellAndGetCommandOnlyStdout(new GeneralCommandLine("printenv", "SHELL"), options, DEFAULT_TIMEOUT, true);
681   }
682
683   @NotNull ProcessOutput executeInShellAndGetCommandOnlyStdout(@NotNull GeneralCommandLine commandLine,
684                                                                @NotNull WSLCommandLineOptions options,
685                                                                int timeout) throws ExecutionException {
686     if (!options.isExecuteCommandInShell()) {
687       throw new AssertionError("Execution in shell is expected");
688     }
689     // When command is executed in interactive/login shell, the result stdout may contain additional output
690     // produced by shell configuration files, for example, "Message Of The Day".
691     // Let's print some unique message before executing the command to know where command output begins in the result output.
692     String prefixText = "intellij: executing command...";
693     options.addInitCommand("echo " + CommandLineUtil.posixQuote(prefixText));
694     if (options.isExecuteCommandInInteractiveShell()) {
695       // Disable oh-my-zsh auto update on shell initialization
696       commandLine.getEnvironment().put(EnvironmentUtil.DISABLE_OMZ_AUTO_UPDATE, "true");
697       options.setPassEnvVarsUsingInterop(true);
698     }
699     ProcessOutput output = executeOnWsl(commandLine, options, timeout);
700     String stdout = output.getStdout();
701     String markerText = prefixText + LineSeparator.LF.getSeparatorString();
702     int index = stdout.indexOf(markerText);
703     if (index < 0) {
704       Application application = ApplicationManager.getApplication();
705       if (application == null || application.isInternal() || application.isUnitTestMode()) {
706         LOG.error("Cannot find '" + prefixText + "' in stdout: " + output);
707       }
708       else {
709         LOG.info("Cannot find '" + prefixText + "' in stdout");
710       }
711       return output;
712     }
713     return new ProcessOutput(stdout.substring(index + markerText.length()),
714                              output.getStderr(),
715                              output.getExitCode(),
716                              output.isTimeout(),
717                              output.isCancelled());
718   }
719
720   @SuppressWarnings("SameParameterValue")
721   @Nullable String executeInShellAndGetCommandOnlyStdout(@NotNull GeneralCommandLine commandLine,
722                                                          @NotNull WSLCommandLineOptions options,
723                                                          int timeout,
724                                                          boolean expectOneLineStdout) {
725     try {
726       ProcessOutput output = executeInShellAndGetCommandOnlyStdout(commandLine, options, timeout);
727       String stdout = output.getStdout();
728       if (!output.isTimeout() && output.getExitCode() == 0) {
729         return expectOneLineStdout ? expectOneLineOutput(commandLine, stdout) : stdout;
730       }
731       LOG.info("Failed to execute " + commandLine + " for " + getMsId() + ": " +
732                "exitCode=" + output.getExitCode() + ", timeout=" + output.isTimeout() +
733                ", stdout=" + stdout + ", stderr=" + output.getStderr());
734     }
735     catch (ExecutionException e) {
736       LOG.info("Failed to execute " + commandLine + " for " + getMsId(), e);
737     }
738     return null;
739   }
740
741   private @NotNull String expectOneLineOutput(@NotNull GeneralCommandLine commandLine, @NotNull String stdout) {
742     String converted = StringUtil.convertLineSeparators(stdout, LineSeparator.LF.getSeparatorString());
743     List<String> lines = StringUtil.split(converted, LineSeparator.LF.getSeparatorString(), true, true);
744     if (lines.size() != 1) {
745       LOG.info("One line stdout expected: " + getMsId() + ", command=" + commandLine + ", stdout=" + stdout + ", lines=" + lines.size());
746     }
747     return StringUtil.notNullize(ContainerUtil.getFirstItem(lines), stdout);
748   }
749 }