mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-04-18 20:41:22 +07:00
[python][exec service] add executeInteractive (PY-60410)
To work with stdin/stdout execute interactive allows direct access to the EelProcess. GitOrigin-RevId: 81cfbded1a9e547bdecdd6224d13cd4bc7184892
This commit is contained in:
committed by
intellij-monorepo-bot
parent
026730f909
commit
37dcd01f72
@@ -23,5 +23,6 @@
|
||||
<orderEntry type="library" scope="TEST" name="hamcrest" level="project" />
|
||||
<orderEntry type="library" scope="TEST" name="JUnit5Params" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.core" />
|
||||
<orderEntry type="module" module-name="intellij.platform.eel" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -4,6 +4,7 @@ package com.intellij.python.community.execService
|
||||
import com.intellij.execution.process.ProcessOutput
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.platform.eel.EelApi
|
||||
import com.intellij.platform.eel.EelProcess
|
||||
import com.intellij.python.community.execService.impl.ExecServiceImpl
|
||||
import com.jetbrains.python.PythonBinary
|
||||
import com.jetbrains.python.Result
|
||||
@@ -20,6 +21,8 @@ import kotlin.time.Duration.Companion.minutes
|
||||
*/
|
||||
typealias ProcessOutputTransformer<T> = (ProcessOutput) -> Result<T, @NlsSafe String?>
|
||||
|
||||
typealias EelProcessInteractiveHandler<T> = suspend (EelProcess) -> Result<T, @NlsSafe String?>
|
||||
|
||||
object ZeroCodeStdoutTransformer : ProcessOutputTransformer<String> {
|
||||
override fun invoke(processOutput: ProcessOutput): Result<String, String?> =
|
||||
if (processOutput.exitCode == 0) Result.success(processOutput.stdout) else Result.failure(null)
|
||||
@@ -31,6 +34,15 @@ object ZeroCodeStdoutTransformer : ProcessOutputTransformer<String> {
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
interface ExecService {
|
||||
|
||||
@CheckReturnValue
|
||||
suspend fun <T> executeInteractive(
|
||||
whatToExec: WhatToExec,
|
||||
args: List<String> = emptyList(),
|
||||
options: ExecOptions = ExecOptions(),
|
||||
eelProcessInteractiveHandler: EelProcessInteractiveHandler<T>,
|
||||
): Result<T, ExecException>
|
||||
|
||||
/**
|
||||
* Execute [whatToExec] with [args] and get both stdout/stderr outputs if `errorCode != 0`, gets error otherwise.
|
||||
* If you want to show a modal window with progress, use `withModalProgress`.
|
||||
|
||||
@@ -3,116 +3,162 @@ package com.intellij.python.community.execService.impl
|
||||
|
||||
import com.intellij.execution.process.ProcessOutput
|
||||
import com.intellij.openapi.diagnostic.fileLogger
|
||||
import com.intellij.platform.eel.EelApi
|
||||
import com.intellij.platform.eel.execute
|
||||
import com.intellij.platform.eel.getOr
|
||||
import com.intellij.platform.eel.*
|
||||
import com.intellij.platform.eel.path.EelPath
|
||||
import com.intellij.platform.eel.provider.getEelDescriptor
|
||||
import com.intellij.platform.eel.provider.utils.*
|
||||
import com.intellij.python.community.execService.ExecOptions
|
||||
import com.intellij.python.community.execService.ExecService
|
||||
import com.intellij.python.community.execService.ProcessOutputTransformer
|
||||
import com.intellij.python.community.execService.WhatToExec
|
||||
import com.intellij.python.community.execService.*
|
||||
import com.jetbrains.python.PythonHelpersLocator
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.PyError.ExecException
|
||||
import com.jetbrains.python.errorProcessing.failure
|
||||
import com.jetbrains.python.execution.FailureReason
|
||||
import com.jetbrains.python.execution.PyExecutionFailure
|
||||
import com.jetbrains.python.execution.userMessage
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.jetbrains.annotations.CheckReturnValue
|
||||
import org.jetbrains.annotations.Nls
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.pathString
|
||||
import kotlin.time.Duration
|
||||
|
||||
private val WhatToExec.defaultProcessName: @Nls String
|
||||
get() = when (this) {
|
||||
is WhatToExec.Binary -> PyExecBundle.message("py.exec.defaultName.process")
|
||||
is WhatToExec.Helper -> PyExecBundle.message("py.exec.defaultName.helper")
|
||||
is WhatToExec.Command -> PyExecBundle.message("py.exec.defaultName.command")
|
||||
}
|
||||
|
||||
internal object ExecServiceImpl : ExecService {
|
||||
override suspend fun <T> executeInteractive(
|
||||
whatToExec: WhatToExec,
|
||||
args: List<String>,
|
||||
options: ExecOptions,
|
||||
eelProcessInteractiveHandler: EelProcessInteractiveHandler<T>,
|
||||
): Result<T, ExecException> {
|
||||
val executableProcess = whatToExec.buildExecutableProcess(args, options)
|
||||
val eelProcess = executableProcess.run().getOr { return it }
|
||||
|
||||
val result = try {
|
||||
withTimeout(options.timeout) {
|
||||
val interactiveResult = eelProcessInteractiveHandler.invoke(eelProcess)
|
||||
val exitProcessOutput = eelProcess.awaitProcessResult().asPlatformOutput()
|
||||
|
||||
val successResult = interactiveResult.getOr { failure ->
|
||||
return@withTimeout executableProcess.failAsExecutionFailed(exitProcessOutput, failure.error)
|
||||
}
|
||||
Result.success(successResult)
|
||||
}
|
||||
}
|
||||
catch (_: TimeoutCancellationException) {
|
||||
executableProcess.killProcessAndFailAsTimeout(eelProcess, options.timeout)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
override suspend fun <T> execute(
|
||||
whatToExec: WhatToExec,
|
||||
args: List<String>,
|
||||
options: ExecOptions,
|
||||
processOutputTransformer: ProcessOutputTransformer<T>,
|
||||
): Result<T, ExecException> {
|
||||
val (eel, exe, args) = when (whatToExec) {
|
||||
is WhatToExec.Binary -> Triple(whatToExec.binary.getEelDescriptor().upgrade(), whatToExec.binary.pathString, args)
|
||||
is WhatToExec.Helper -> {
|
||||
val eel = whatToExec.python.getEelDescriptor().upgrade()
|
||||
val localHelper = PythonHelpersLocator.findPathInHelpers(whatToExec.helper)
|
||||
?: error("No ${whatToExec.helper} found: installation broken?")
|
||||
val remoteHelper = EelPathUtils.transferLocalContentToRemoteTempIfNeeded(eel, localHelper).toString()
|
||||
Triple(eel, whatToExec.python.pathString, listOf(remoteHelper) + args)
|
||||
}
|
||||
is WhatToExec.Command -> Triple(whatToExec.eel, whatToExec.command, args)
|
||||
val executableProcess = whatToExec.buildExecutableProcess(args, options)
|
||||
val eelProcess = executableProcess.run().getOr { return it }
|
||||
|
||||
val eelProcessExecutionResult = try {
|
||||
withTimeout(options.timeout) { eelProcess.awaitProcessResult() }
|
||||
}
|
||||
catch (_: TimeoutCancellationException) {
|
||||
return executableProcess.killProcessAndFailAsTimeout(eelProcess, options.timeout)
|
||||
}
|
||||
|
||||
val processOutput = eel.execGetProcessOutputImpl(
|
||||
exe = exe,
|
||||
args = args,
|
||||
options = options,
|
||||
processDescription = options.processDescription ?: whatToExec.defaultProcessName
|
||||
).getOr { return it }
|
||||
|
||||
val transformerResult = processOutputTransformer.invoke(processOutput)
|
||||
val transformerSuccess = transformerResult.getOr { transformerFailure ->
|
||||
val additionalMessage = transformerFailure.error ?: run {
|
||||
val actualProcessDescription = options.processDescription ?: whatToExec.defaultProcessName
|
||||
PyExecBundle.message("py.exec.exitCode.error", actualProcessDescription, processOutput.exitCode)
|
||||
}
|
||||
|
||||
return PyExecFailureImpl(
|
||||
command = exe,
|
||||
args = args,
|
||||
additionalMessage = additionalMessage,
|
||||
failureReason = FailureReason.ExecutionFailed(processOutput)
|
||||
).let {
|
||||
fileLogger().warn(it.userMessage)
|
||||
failure(it)
|
||||
}
|
||||
val processOutput = eelProcessExecutionResult.asPlatformOutput()
|
||||
val transformerSuccess = processOutputTransformer.invoke(processOutput).getOr { failure ->
|
||||
return executableProcess.failAsExecutionFailed(processOutput, failure.error)
|
||||
}
|
||||
return Result.success(transformerSuccess)
|
||||
}
|
||||
}
|
||||
|
||||
private data class EelExecutableProcess(
|
||||
val eel: EelApi,
|
||||
val exe: String,
|
||||
val args: List<String>,
|
||||
val env: Map<String, String>,
|
||||
val workingDirectory: Path?,
|
||||
val description: @Nls String,
|
||||
)
|
||||
|
||||
private suspend fun WhatToExec.buildExecutableProcess(args: List<String>, options: ExecOptions): EelExecutableProcess {
|
||||
val (eel, exe, args) = when (this) {
|
||||
is WhatToExec.Binary -> Triple(binary.getEelDescriptor().upgrade(), binary.pathString, args)
|
||||
is WhatToExec.Helper -> {
|
||||
val eel = python.getEelDescriptor().upgrade()
|
||||
val localHelper = PythonHelpersLocator.findPathInHelpers(helper)
|
||||
?: error("No ${helper} found: installation broken?")
|
||||
val remoteHelper = EelPathUtils.transferLocalContentToRemoteTempIfNeeded(eel, localHelper).toString()
|
||||
Triple(eel, python.pathString, listOf(remoteHelper) + args)
|
||||
}
|
||||
is WhatToExec.Command -> Triple(eel, command, args)
|
||||
}
|
||||
|
||||
val description = options.processDescription ?: when (this) {
|
||||
is WhatToExec.Binary -> PyExecBundle.message("py.exec.defaultName.process")
|
||||
is WhatToExec.Helper -> PyExecBundle.message("py.exec.defaultName.helper")
|
||||
is WhatToExec.Command -> PyExecBundle.message("py.exec.defaultName.command")
|
||||
}
|
||||
|
||||
return EelExecutableProcess(eel, exe, args, options.env, options.workingDirectory, description)
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
@CheckReturnValue
|
||||
private suspend fun EelApi.execGetProcessOutputImpl(
|
||||
exe: String,
|
||||
args: List<String>,
|
||||
options: ExecOptions,
|
||||
processDescription: @Nls String,
|
||||
): Result<ProcessOutput, ExecException> {
|
||||
val workDirectoryEelPath = options.workingDirectory?.let { EelPath.parse(it.toString(), this.descriptor) }
|
||||
val executionResult = exec.execute(exe) {
|
||||
private suspend fun EelExecutableProcess.run(): Result<EelProcess, ExecException> {
|
||||
val workDirectoryEelPath = workingDirectory?.let { EelPath.parse(it.toString(), eel.descriptor) }
|
||||
val executionResult = eel.exec.execute(exe) {
|
||||
args(args)
|
||||
env(options.env)
|
||||
env(env)
|
||||
workingDirectory(workDirectoryEelPath)
|
||||
}
|
||||
|
||||
val process = executionResult.getOr { err ->
|
||||
val text = PyExecBundle.message("py.exec.start.error", processDescription, err.error.message, err.error.errno)
|
||||
val failure = PyExecFailureImpl(exe, args, text, FailureReason.CantStart)
|
||||
fileLogger().warn(failure.userMessage)
|
||||
return failure(failure)
|
||||
return failAsCantStart(err.error)
|
||||
}
|
||||
val result = withTimeoutOrNull(options.timeout) {
|
||||
process.awaitProcessResult()
|
||||
}
|
||||
if (result == null) {
|
||||
process.kill()
|
||||
val text = PyExecBundle.message("py.exec.timeout.error", processDescription, options.timeout)
|
||||
val failure = PyExecFailureImpl(exe, args, text, FailureReason.CantStart)
|
||||
fileLogger().info(failure.userMessage)
|
||||
return failure(failure)
|
||||
}
|
||||
|
||||
return Result.success(result.asPlatformOutput())
|
||||
return Result.success(process)
|
||||
}
|
||||
|
||||
private fun EelExecutableProcess.failAsCantStart(executeProcessError: EelExecApi.ExecuteProcessError): Result.Failure<ExecException> {
|
||||
return PyExecFailureImpl(
|
||||
command = exe,
|
||||
args = args,
|
||||
additionalMessage = PyExecBundle.message("py.exec.start.error", description, executeProcessError.message, executeProcessError.errno),
|
||||
failureReason = FailureReason.CantStart
|
||||
).logAndFail()
|
||||
}
|
||||
|
||||
private suspend fun EelExecutableProcess.killProcessAndFailAsTimeout(eelProcess: EelProcess, timeout: Duration): Result.Failure<ExecException> {
|
||||
eelProcess.kill()
|
||||
|
||||
return PyExecFailureImpl(
|
||||
command = exe,
|
||||
args = args,
|
||||
additionalMessage = PyExecBundle.message("py.exec.timeout.error", description, timeout),
|
||||
failureReason = FailureReason.CantStart
|
||||
).logAndFail()
|
||||
}
|
||||
|
||||
private fun EelExecutableProcess.failAsExecutionFailed(processOutput: ProcessOutput, customMessage: @Nls String?): Result.Failure<ExecException> {
|
||||
val additionalMessage = customMessage ?: run {
|
||||
PyExecBundle.message("py.exec.exitCode.error", description, processOutput.exitCode)
|
||||
}
|
||||
|
||||
return PyExecFailureImpl(
|
||||
command = exe,
|
||||
args = args,
|
||||
additionalMessage = additionalMessage,
|
||||
failureReason = FailureReason.ExecutionFailed(processOutput)
|
||||
).logAndFail()
|
||||
}
|
||||
|
||||
private fun PyExecutionFailure.logAndFail(): Result.Failure<ExecException> {
|
||||
fileLogger().warn(userMessage)
|
||||
return failure(this)
|
||||
}
|
||||
|
||||
|
||||
private fun EelProcessExecutionResult.asPlatformOutput(): ProcessOutput = ProcessOutput(stdoutString, stderrString, exitCode, false, false)
|
||||
Reference in New Issue
Block a user