DS-4175: Workaround for symlinks on WSL

``/usr/bin/python`` is symlink in many distros and can't be checked via 9P (``\\wsl$``). Hence, we provide ``Unknown`` for such cases.
See issue comment for more info

(cherry picked from commit 529eec2203f21e91c1ebc4f3df5d9b22ce4fcf3b)

IJ-MR-98852

(cherry picked from commit e4954edbc1d87709387ebaaf0a2cac0f150c5d05)

GitOrigin-RevId: d6b4b955a14e38fe217a20f33e6260c050af0984
This commit is contained in:
Ilya.Kazakevich
2022-11-18 14:51:31 +01:00
committed by intellij-monorepo-bot
parent 03adfecf20
commit 2e6c91c3ea
7 changed files with 66 additions and 4 deletions

View File

@@ -13,6 +13,8 @@ import com.intellij.execution.wsl.listWindowsRoots
import com.intellij.openapi.components.BaseState
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.util.SystemInfoRt
import com.sun.jna.platform.win32.Kernel32.*
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.io.path.pathString
@@ -65,13 +67,21 @@ class WslTargetEnvironmentConfiguration() : TargetEnvironmentConfiguration(WslTa
}
override fun getPathInfo(targetPath: String): PathInfo? {
// TODO: 9P is unreliable and we must migrate to some tool running in WSL (like ijent)
assert(SystemInfoRt.isWindows) { "WSL is for Windows only" }
val distribution = distribution
if (distribution == null) {
thisLogger().warn("No distribution, cant check path")
return null
}
val pathInfo = PathInfo.getPathInfoForLocalPath(Paths.get(distribution.getWindowsPath(targetPath)))
// We can't check if file is executable or not (we could but it is too heavy), so we set this flag
val winLocalPath = Paths.get(distribution.getWindowsPath(targetPath))
val fileAttributes = INSTANCE.GetFileAttributes(winLocalPath.pathString)
// Reparse point is probably symlink, but could be dir or file. See https://github.com/microsoft/WSL/issues/5118
if (fileAttributes != INVALID_FILE_ATTRIBUTES && fileAttributes.and(FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT) {
return PathInfo.Unknown
}
val pathInfo = PathInfo.getPathInfoForLocalPath(winLocalPath)
// We can't check if file is executable or not (we could, but it is too heavy), so we set this flag
return if (pathInfo is PathInfo.RegularFile) pathInfo.copy(executable = true) else pathInfo
}

View File

@@ -9,6 +9,10 @@ import kotlin.io.path.*
* Abstraction over target path because target paths (like ssh or wsl) can't always be represented as [Path].
*/
sealed class PathInfo {
/**
* File system object exists, but we do not know what is it
*/
object Unknown: PathInfo()
data class Directory(val empty: Boolean) : PathInfo()
data class RegularFile(val executable: Boolean) : PathInfo()
companion object {

View File

@@ -72,6 +72,7 @@
<orderEntry type="module" module-name="intellij.platform.testFramework.impl" scope="TEST" />
<orderEntry type="library" name="kotlin-reflect" level="project" />
<orderEntry type="module" module-name="intellij.platform.util.ui" scope="TEST" />
<orderEntry type="library" scope="TEST" name="hamcrest" level="project" />
<orderEntry type="library" scope="TEST" name="HdrHistogram" level="project" />
<orderEntry type="module" module-name="intellij.platform.util.text.matching" scope="TEST" />
</component>

View File

@@ -0,0 +1,42 @@
// 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.execution.target.TargetConfigurationWithLocalFsAccess
import com.intellij.execution.target.readableFs.PathInfo.*
import com.intellij.execution.wsl.target.WslTargetEnvironmentConfiguration
import com.intellij.testFramework.fixtures.TestFixtureRule
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.anyOf
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.ClassRule
import org.junit.Test
import org.junit.rules.RuleChain
/**
* Test [TargetConfigurationWithLocalFsAccess] for WSL
*/
class WslPathInfoTest {
companion object {
private val appRule = TestFixtureRule()
private val wslRule = WslRule()
private val wslTempDirRule = WslTempDirRule(wslRule)
@ClassRule
@JvmField
val ruleChain: RuleChain = RuleChain.outerRule(appRule).around(wslRule).around(wslTempDirRule)
}
@Test
fun testPathInfo() {
val target = WslTargetEnvironmentConfiguration(wslRule.wsl)
assertNull("Path doesn't exist", target.getPathInfo("/path/doesn/exist"))
assertThat("Directory", target.getPathInfo("/bin"), anyOf(`is`(Unknown), `is`(Directory(false))))
assertThat("Directory", target.getPathInfo("/usr/bin"), anyOf(`is`(Unknown), `is`(Directory(false))))
assertThat("Executable file", target.getPathInfo("/bin/sh"), anyOf(`is`(Unknown), `is`(RegularFile(true))))
assertThat("Not executable file", target.getPathInfo("/etc/resolv.conf"), anyOf(`is`(Unknown), `is`(RegularFile(false))))
val emptyDir = wslTempDirRule.linuxPath
assertEquals("Empty dir", Directory(true), target.getPathInfo(emptyDir))
}
}

View File

@@ -146,7 +146,7 @@ public abstract class PythonSdkFlavor<D extends PyFlavorData> {
}
/**
* False means file is not executable, but true means it is executable or we do not know.
* False means file is not executable, but true means it is executable, or we do not know.
*
* @param fullPath full path on target
*/
@@ -157,6 +157,9 @@ public abstract class PythonSdkFlavor<D extends PyFlavorData> {
}
if (targetEnvConfig instanceof TargetConfigurationReadableFs) {
var fileInfo = ((TargetConfigurationReadableFs)targetEnvConfig).getPathInfo(fullPath);
if (fileInfo instanceof PathInfo.Unknown) {
return true; // We can't be sure if file is executable or not
}
return (fileInfo instanceof PathInfo.RegularFile) && (((PathInfo.RegularFile)fileInfo).getExecutable());
}
// We can't be sure if file is executable or not

View File

@@ -32,6 +32,7 @@ fun validateExecutableFile(
request: ValidationRequest
): ValidationInfo? = request.validate {
when (it) {
is PathInfo.Unknown -> null
is PathInfo.RegularFile -> if (it.executable) null else PyBundle.message("python.sdk.cannot.execute", request.path)
is PathInfo.Directory -> PyBundle.message("python.sdk.cannot.execute", request.path)
else -> PyBundle.message("python.sdk.file.not.found", request.path)
@@ -46,6 +47,7 @@ fun validateEmptyDir(request: ValidationRequest,
@Nls directoryNotEmpty: String
): ValidationInfo? = request.validate {
when (it) {
is PathInfo.Unknown -> null
is PathInfo.Directory -> if (it.empty) null else directoryNotEmpty
is PathInfo.RegularFile -> notADirectory
else -> null

View File

@@ -22,7 +22,7 @@ class PyCondaCommand(
if (pathInfo == null) {
return Result.failure(Exception("$fullCondaPathOnTarget does not exist"))
}
if ((pathInfo as? PathInfo.RegularFile)?.executable != true) {
if (pathInfo != PathInfo.Unknown && (pathInfo as? PathInfo.RegularFile)?.executable != true) {
return Result.failure(Exception("$fullCondaPathOnTarget is not executable file"))
}
}