From f6822cd05fabc20f7f5315743d50ecd2454fa034 Mon Sep 17 00:00:00 2001 From: Vitaly Legchilkin Date: Thu, 19 Mar 2026 15:50:07 +0100 Subject: [PATCH] PY-88507: fix SSH executable validation using POSIX-portable shell Use the login shell (with /bin/sh fallback) to run `test -f` instead of directly invoking `/usr/bin/test`, which may not exist on minimal remote systems or Docker containers. Also simplify `executeCommand` to return `PyResult` and `getShell` to return `String` with a `/bin/sh` fallback, removing unnecessary error wrapping. (cherry picked from commit a03e1156dd81bf4398c763dfd009bd23061973be) IJ-MR-196962 GitOrigin-RevId: 9f78a5d13a864f1b1701fab344a437583a4a2667 --- .../jetbrains/python/sdk/add/v2/FileSystem.kt | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt b/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt index 61f023b7570d..bfde3c041cf5 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt @@ -20,7 +20,6 @@ import com.intellij.python.community.execService.BinOnTarget import com.intellij.python.community.execService.BinaryToExec import com.intellij.python.community.execService.ExecService import com.intellij.python.community.execService.execGetStdout -import com.intellij.python.community.execService.execute import com.intellij.python.community.execService.python.validatePythonAndGetInfo import com.intellij.python.community.services.internal.impl.VanillaPythonWithPythonInfoImpl import com.intellij.python.community.services.shared.VanillaPythonWithPythonInfo @@ -33,6 +32,7 @@ import com.jetbrains.python.Result import com.jetbrains.python.errorProcessing.MessageError import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.isCondaVirtualEnv +import com.jetbrains.python.isSuccess import com.jetbrains.python.orLogException import com.jetbrains.python.pathValidation.PlatformAndRoot.Companion.getPlatformAndRoot import com.jetbrains.python.pathValidation.ValidationRequest @@ -262,7 +262,7 @@ sealed interface FileSystem

{ override val isLocal: Boolean = false private val systemPythonCache = ArrayList>() - private lateinit var shellImpl: PyResult + private lateinit var shellImpl: String override fun parsePath(raw: String): PyResult { return PyResult.success(PathHolder.Target(raw)) @@ -278,8 +278,7 @@ sealed interface FileSystem

{ else PyResult.localizedError(message("sdk.create.not.executable.does.not.exist.error")) override suspend fun fileExists(path: PathHolder.Target): Boolean { - val bin = getBinaryToExec(PathHolder.Target("/usr/bin/test")) - return ExecService().execute(bin, Args("-f", path.pathString), processOutputTransformer = { output -> PyResult.success(output.exitCode == 0) }).successOrNull ?: false + return executeCommand("test -f ${path.pathString}").isSuccess } override suspend fun validateVenv(homePath: PathHolder.Target): PyResult = withContext(Dispatchers.IO) { @@ -373,22 +372,22 @@ sealed interface FileSystem

{ } override suspend fun which(cmd: String): PathHolder.Target? { - val binaryPathString = executeCommand("which $cmd") ?: return null + val binaryPathString = executeCommand("which $cmd").getOr { return null } val binaryPathOnFS = parsePath(binaryPathString).getOr { return null } return binaryPathOnFS } - override suspend fun getHomePath(): PathHolder.Target? = executeCommand($$"echo ${HOME}")?.let { PathHolder.Target(it) } + override suspend fun getHomePath(): PathHolder.Target? = executeCommand($$"echo ${HOME}").successOrNull?.let { PathHolder.Target(it) } - private suspend fun executeCommand(cmd: String): String? { - val shell = getShell().getOr { return null } + private suspend fun executeCommand(cmd: String): PyResult { + val shell = getShell() val bin = getBinaryToExec(PathHolder.Target(shell)) - return ExecService().execGetStdout(bin, Args("-l", "-c", cmd)).successOrNull + return ExecService().execGetStdout(bin, Args("-l", "-c", cmd)) } - private suspend fun getShell(): PyResult { + private suspend fun getShell(): String { if (!this::shellImpl.isInitialized) { - shellImpl = getShellImpl() + shellImpl = getShellImpl().orLogException(LOG) ?: "/bin/sh" } return shellImpl }