Python: refactor validatePythonAndGetVersion to become eel-compatible

GitOrigin-RevId: 8d6e71dcd5694f98a7763204c8848d175c8ea78b
This commit is contained in:
Ilya.Kazakevich
2024-12-23 22:37:33 +01:00
committed by intellij-monorepo-bot
parent 849ae5b514
commit 1aec794bc8
3 changed files with 32 additions and 44 deletions

View File

@@ -33,5 +33,8 @@
<orderEntry type="library" name="jackson-module-kotlin" level="project" />
<orderEntry type="module" module-name="intellij.platform.ide.progress" />
<orderEntry type="library" scope="TEST" name="JUnit5" level="project" />
<orderEntry type="module" module-name="intellij.platform.eel.impl" />
<orderEntry type="module" module-name="intellij.platform.eel.provider" />
<orderEntry type="module" module-name="intellij.platform.eel" />
</component>
</module>

View File

@@ -85,4 +85,5 @@ path.validation.invalid=Path is invalid: {0}
path.validation.inaccessible=Path is inaccessible
python.get.version.error={0} returned error: {1}
python.get.version.too.long={0} took too long
python.get.version.wrong.version={0} has a wrong version: {1}

View File

@@ -1,69 +1,53 @@
package com.jetbrains.python
import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.util.io.awaitExit
import com.intellij.openapi.util.NlsSafe
import com.intellij.platform.eel.getOr
import com.intellij.platform.eel.impl.utils.exec
import com.jetbrains.python.PySdkBundle.message
import com.jetbrains.python.Result.Companion.failure
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor.PYTHON_VERSION_ARG
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor.getVersionStringFromOutput
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import org.jetbrains.annotations.ApiStatus
import kotlin.io.path.pathString
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
// TODO: PythonInterpreterService: validate system python
/**
* Ensures that this python is executable and returns its version. Error if python is broken (reports error to logs as well).
* Ensures that this python is executable and returns its version. Error if python is broken.
*
* Some pythons might be broken: they may be executable, even return a version, but still fail to execute it.
* As we need workable pythons, we validate it by executing
*/
@ApiStatus.Internal
suspend fun PythonBinary.validatePythonAndGetVersion(): Result<LanguageLevel, LocalizedErrorString> = withContext(Dispatchers.IO) {
val fileLogger = fileLogger()
val process =
try {
GeneralCommandLine(pathString, "-c", "print(1)").createProcess()
}
catch (e: ExecutionException) {
val error = message("python.get.version.error", pathString, e.message)
fileLogger.warn(error, e)
return@withContext failure(LocalizedErrorString(error))
}
val timeout = 5.seconds
val exitCode = withTimeoutOrNull(timeout) {
process.awaitExit()
val smokeTestOutput = executeWithResult("-c", "print(1)").getOr { return@withContext it }.trim()
if (smokeTestOutput != "1") {
return@withContext failure(LocalizedErrorString(message("python.get.version.error", pathString, smokeTestOutput)))
}
when (exitCode) {
null -> {
fileLogger.warn("$this didn't return in $timeout, skipping")
}
0 -> {
val pythonVersion = PythonSdkFlavor.getVersionStringStatic(pathString)
?: return@withContext failure(LocalizedErrorString(message("python.get.version.wrong.version", pathString, "")))
val languageLevel = LanguageLevel.fromPythonVersion(pythonVersion)
if (languageLevel == null) {
fileLogger.warn("$pythonVersion is not valid version")
return@withContext failure(LocalizedErrorString(message("python.get.version.wrong.version", pathString, "")))
}
return@withContext Result.success(languageLevel)
}
else -> {
fileLogger.warn("$this exited with code ${exitCode}, skipping")
}
val versionString = executeWithResult(PYTHON_VERSION_ARG).getOr { return@withContext it }
val languageLevel = getVersionStringFromOutput(versionString)?.let {
LanguageLevel.fromPythonVersion(it)
}
process.destroyForcibly()
if (withTimeoutOrNull(500.milliseconds) {
process.awaitExit()
} == null) {
fileLogger.warn("Process $process still running, might be leaked")
if (languageLevel == null) {
return@withContext failure(LocalizedErrorString(message("python.get.version.wrong.version", pathString, versionString)))
}
return@withContext failure(LocalizedErrorString(message("python.get.version.error", pathString, exitCode)))
return@withContext Result.success(languageLevel)
}
private suspend fun PythonBinary.executeWithResult(vararg args: String): Result<@NlsSafe String, LocalizedErrorString> {
val output = exec(*args, timeout = 5.seconds).getOr {
val text = it.error?.message ?: message("python.get.version.too.long", pathString)
return failure(LocalizedErrorString(text))
}
return if (output.exitCode != 0) {
failure(LocalizedErrorString(message("python.get.version.error", pathString, "code ${output.exitCode}, {output.stderr}")))
}
else {
Result.success(output.stdout)
}
}