mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Python: Support Targets API in ExecService.
API changes: Class `Args` is not used to provide arguments for commands. Previous implementation was incompatible with Targets. Instead of `Path` we now accept `BinaryToExec` which is either Eel or Target based. Impl: `ExecServiceImpl` now delegates execution to `com.intellij.python.community.execService.impl.processLaunchers`: there are two functions (Eel and Targets) Merge-request: IJ-MR-171488 Merged-by: Ilya Kazakevich <ilya.kazakevich@jetbrains.com> GitOrigin-RevId: 4c4ac7023e6605caaacb1880d60faf145b4160bf
This commit is contained in:
committed by
intellij-monorepo-bot
parent
bc59f691df
commit
8f83b52355
@@ -8,10 +8,20 @@ import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
|
||||
@ApiStatus.Internal
|
||||
data class ProcessFunctions(
|
||||
class ProcessFunctions(
|
||||
val waitForExit: suspend () -> Unit,
|
||||
val killProcess: suspend () -> Unit,
|
||||
)
|
||||
private val killProcess: suspend () -> Unit,
|
||||
) {
|
||||
suspend fun killAndJoin(logger: Logger, processNameForDebug: String) {
|
||||
withContext(NonCancellable) {
|
||||
logger.warn("Sending kill to $processNameForDebug")
|
||||
killProcess()
|
||||
logger.warn("Kill send to $processNameForDebug, waiting")
|
||||
waitForExit()
|
||||
logger.warn("Process $processNameForDebug died")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an implementation detail to be reused by other parts of a system.
|
||||
@@ -26,13 +36,7 @@ fun CoroutineScope.bindProcessToScopeImpl(
|
||||
val context = CoroutineName("Waiting for process $processNameForDebug") + Dispatchers.IO
|
||||
|
||||
suspend fun killAndJoin() {
|
||||
withContext(NonCancellable) {
|
||||
logger.warn("Sending kill to $processNameForDebug")
|
||||
processFunctions.killProcess()
|
||||
logger.warn("Kill send to $processNameForDebug, waiting")
|
||||
processFunctions.waitForExit()
|
||||
logger.warn("Process $processNameForDebug died")
|
||||
}
|
||||
processFunctions.killAndJoin(logger, processNameForDebug)
|
||||
}
|
||||
|
||||
if (!isActive) {
|
||||
|
||||
@@ -7,7 +7,6 @@ import com.intellij.python.community.execService.python.advancedApi.executeHelpe
|
||||
import com.intellij.python.community.execService.python.advancedApi.executePythonAdvanced
|
||||
import com.intellij.python.community.execService.python.advancedApi.validatePythonAndGetVersion
|
||||
import com.intellij.python.community.interpreters.ValidInterpreter
|
||||
import com.jetbrains.python.errorProcessing.PyExecResult
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
@@ -19,11 +18,11 @@ import org.jetbrains.annotations.ApiStatus
|
||||
*/
|
||||
suspend fun <T> ExecService.executePythonAdvanced(
|
||||
python: ValidInterpreter,
|
||||
argsBuilder: suspend ArgsBuilder.() -> Unit = {},
|
||||
args: Args,
|
||||
options: ExecOptions = ExecOptions(),
|
||||
processInteractiveHandler: ProcessInteractiveHandler<T>,
|
||||
): PyResult<T> =
|
||||
executePythonAdvanced(python.asExecutablePython, argsBuilder, options, processInteractiveHandler)
|
||||
executePythonAdvanced(python.asExecutablePython, args, options, processInteractiveHandler)
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
package com.intellij.python.community.interpreters
|
||||
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.python.community.execService.ExecOptions
|
||||
import com.intellij.python.community.execService.ExecService
|
||||
import com.intellij.python.community.execService.PyProcessListener
|
||||
import com.intellij.python.community.execService.ZeroCodeStdoutTransformer
|
||||
import com.intellij.python.community.execService.*
|
||||
import com.intellij.python.community.execService.impl.transformerToHandler
|
||||
import com.intellij.python.community.execService.python.HelperName
|
||||
import com.intellij.python.community.execService.python.advancedApi.executeHelperAdvanced
|
||||
@@ -91,4 +88,4 @@ suspend fun ExecService.executeGetStdout(
|
||||
options: ExecOptions = ExecOptions(),
|
||||
procListener: PyProcessListener? = null,
|
||||
): PyResult<String> =
|
||||
executePythonAdvanced(python.asExecutablePython, { addArgs(*args.toTypedArray()) }, options, transformerToHandler(procListener, ZeroCodeStdoutTransformer))
|
||||
executePythonAdvanced(python.asExecutablePython, Args(*args.toTypedArray()), options, transformerToHandler(procListener, ZeroCodeStdoutTransformer))
|
||||
@@ -17,7 +17,7 @@ import kotlin.io.path.Path
|
||||
/**
|
||||
* Exe might sit on eel (new one) or on target (legacy)
|
||||
*/
|
||||
interface Exe {
|
||||
sealed interface Exe {
|
||||
companion object {
|
||||
fun fromString(path: String): Exe {
|
||||
try {
|
||||
|
||||
@@ -23,6 +23,8 @@ jvm_library(
|
||||
"//platform/core-api:core",
|
||||
"//platform/eel",
|
||||
"//platform/util/progress",
|
||||
"//platform/execution",
|
||||
"//platform/projectModel-api:projectModel",
|
||||
],
|
||||
runtime_deps = [":community-execService_resources"]
|
||||
)
|
||||
@@ -53,6 +55,8 @@ jvm_library(
|
||||
"@lib//:junit5Pioneer",
|
||||
"//platform/testFramework/common",
|
||||
"//platform/util/progress",
|
||||
"//platform/execution",
|
||||
"//platform/projectModel-api:projectModel",
|
||||
],
|
||||
runtime_deps = [":community-execService_resources"]
|
||||
)
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.intellij.python.community.execService.impl.transformerToHandler
|
||||
import com.intellij.python.community.execService.python.HelperName
|
||||
import com.intellij.python.community.execService.python.impl.validatePythonAndGetVersionImpl
|
||||
import com.intellij.python.community.helpersLocator.PythonHelpersLocator
|
||||
import com.jetbrains.python.errorProcessing.PyExecResult
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
@@ -18,15 +17,15 @@ import org.jetbrains.annotations.ApiStatus
|
||||
*/
|
||||
suspend fun <T> ExecService.executePythonAdvanced(
|
||||
python: ExecutablePython,
|
||||
argsBuilder: suspend ArgsBuilder.() -> Unit = {},
|
||||
args: Args,
|
||||
options: ExecOptions = ExecOptions(),
|
||||
processInteractiveHandler: ProcessInteractiveHandler<T>,
|
||||
): PyExecResult<T> =
|
||||
executeAdvanced(python.binary, {
|
||||
addArgs(*python.args.toTypedArray())
|
||||
argsBuilder()
|
||||
): PyResult<T> =
|
||||
executeAdvanced(
|
||||
binary = BinOnEel(python.binary),
|
||||
args = Args(*python.args.toTypedArray()) + args,
|
||||
// TODO: Merge PATH
|
||||
}, options.copy(env = options.env + python.env), processInteractiveHandler)
|
||||
options = options.copy(env = options.env + python.env), processInteractiveHandler)
|
||||
|
||||
|
||||
/**
|
||||
@@ -39,11 +38,13 @@ suspend fun <T> ExecService.executeHelperAdvanced(
|
||||
options: ExecOptions = ExecOptions(),
|
||||
procListener: PyProcessListener? = null,
|
||||
processOutputTransformer: ProcessOutputTransformer<T>,
|
||||
): PyExecResult<T> = executePythonAdvanced(python, {
|
||||
addLocalFile(PythonHelpersLocator.findPathInHelpers(helper))
|
||||
addArgs(*args.toTypedArray())
|
||||
|
||||
}, options, transformerToHandler(procListener, processOutputTransformer))
|
||||
): PyResult<T> = executePythonAdvanced(
|
||||
python,
|
||||
Args().apply {
|
||||
addLocalFile(PythonHelpersLocator.findPathInHelpers(helper))
|
||||
addArgs(*args.toTypedArray())
|
||||
},
|
||||
options, transformerToHandler(procListener, processOutputTransformer))
|
||||
|
||||
/**
|
||||
* Ensures that this python is executable and returns its version. Error if python is broken.
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.platform.eel.provider.utils.EelProcessExecutionResult
|
||||
import com.intellij.platform.eel.provider.utils.stderrString
|
||||
import com.intellij.platform.eel.provider.utils.stdoutString
|
||||
import com.intellij.python.community.execService.Args
|
||||
import com.intellij.python.community.execService.ExecOptions
|
||||
import com.intellij.python.community.execService.ExecService
|
||||
import com.intellij.python.community.execService.ZeroCodeStdoutTransformer
|
||||
@@ -28,12 +29,12 @@ import kotlin.time.Duration.Companion.minutes
|
||||
internal suspend fun ExecService.validatePythonAndGetVersionImpl(python: ExecutablePython): PyResult<LanguageLevel> = withContext(Dispatchers.IO) {
|
||||
val options = ExecOptions(timeout = 1.minutes)
|
||||
|
||||
val smokeTestOutput = executePythonAdvanced(python, { addArgs("-c", "print(1)") }, processInteractiveHandler = transformerToHandler(null, ZeroCodeStdoutTransformer), options = options).getOr(message("python.cannot.exec", python.userReadableName)) { return@withContext it }.trim()
|
||||
val smokeTestOutput = executePythonAdvanced(python, Args("-c", "print(1)"), processInteractiveHandler = transformerToHandler(null, ZeroCodeStdoutTransformer), options = options).getOr(message("python.cannot.exec", python.userReadableName)) { return@withContext it }.trim()
|
||||
if (smokeTestOutput != "1") {
|
||||
return@withContext PyResult.localizedError(message("python.get.version.error", python.userReadableName, smokeTestOutput))
|
||||
}
|
||||
|
||||
val versionOutput: EelProcessExecutionResult = executePythonAdvanced(python, options = options, argsBuilder = { addArgs(PYTHON_VERSION_ARG) }, processInteractiveHandler = transformerToHandler<EelProcessExecutionResult>(null, { r ->
|
||||
val versionOutput: EelProcessExecutionResult = executePythonAdvanced(python, options = options, args = Args(PYTHON_VERSION_ARG), processInteractiveHandler = transformerToHandler<EelProcessExecutionResult>(null, { r ->
|
||||
if (r.exitCode == 0) Result.success(r) else Result.failure(message("python.get.version.error", python.userReadableName, r.exitCode))
|
||||
})).getOr { return@withContext it }
|
||||
// Python 2 might return version as stderr, see https://bugs.python.org/issue18338
|
||||
|
||||
@@ -26,5 +26,7 @@
|
||||
<orderEntry type="library" scope="TEST" name="JUnit5Pioneer" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.testFramework.common" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.util.progress" />
|
||||
<orderEntry type="module" module-name="intellij.platform.execution" />
|
||||
<orderEntry type="module" module-name="intellij.platform.projectModel" />
|
||||
</component>
|
||||
</module>
|
||||
+1
-2
@@ -3,7 +3,6 @@ package com.intellij.python.community.execService
|
||||
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Listens for start/stop/std{out,err} events
|
||||
@@ -11,7 +10,7 @@ import java.nio.file.Path
|
||||
typealias PyProcessListener = FlowCollector<ProcessEvent>
|
||||
|
||||
sealed interface ProcessEvent {
|
||||
data class ProcessStarted @ApiStatus.Internal constructor(val binary: Path, val args: List<String>) : ProcessEvent
|
||||
data class ProcessStarted @ApiStatus.Internal constructor(val binary: BinaryToExec, val args: List<String>) : ProcessEvent
|
||||
data class ProcessOutput @ApiStatus.Internal constructor(val stream: OutputType, val line: String) : ProcessEvent
|
||||
data class ProcessEnded @ApiStatus.Internal constructor(val exitCode: Int) : ProcessEvent
|
||||
|
||||
|
||||
+5
-24
@@ -1,22 +1,20 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.community.execService
|
||||
|
||||
import com.intellij.platform.eel.EelProcess
|
||||
import com.intellij.platform.eel.channels.EelSendChannel
|
||||
import com.intellij.platform.eel.provider.utils.EelProcessExecutionResult
|
||||
import com.intellij.python.community.execService.impl.ProcessSemiInteractiveHandlerImpl
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.PyExecResult
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import kotlinx.coroutines.Deferred
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.annotations.CheckReturnValue
|
||||
import org.jetbrains.annotations.Nls
|
||||
import java.nio.file.Path
|
||||
|
||||
|
||||
// This is an advanced API, consider using basic api.kt
|
||||
|
||||
|
||||
/**
|
||||
* Service is a thin wrapper over [com.intellij.platform.eel.EelApi] to execute python tools on local or remote Eel.
|
||||
* to obtain service, use function with same name.
|
||||
@@ -34,18 +32,16 @@ interface ExecService {
|
||||
* You must listen for process stdout/stderr e.t.c.
|
||||
* Use it if you need to get some info from a process before it ends or to interact (i.e. write into stdin).
|
||||
* See [ProcessInteractiveHandler] and [processSemiInteractiveHandler].
|
||||
* [argsBuilder] is a lambda to build args, see [ArgsBuilder]
|
||||
*/
|
||||
@CheckReturnValue
|
||||
suspend fun <T> executeAdvanced(
|
||||
binary: Path,
|
||||
argsBuilder: suspend ArgsBuilder.() -> Unit = {},
|
||||
binary: BinaryToExec,
|
||||
args: Args,
|
||||
options: ExecOptions = ExecOptions(),
|
||||
processInteractiveHandler: ProcessInteractiveHandler<T>,
|
||||
): PyExecResult<T>
|
||||
): PyResult<T>
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Message to be displayed to a user in case of process failure.
|
||||
*/
|
||||
@@ -61,7 +57,7 @@ fun interface ProcessInteractiveHandler<T> {
|
||||
* In latter case returns [EelProcessExecutionResult] (created out of collected output) and optional error message.
|
||||
* If no message returned -- the default one is used.
|
||||
*/
|
||||
suspend fun getResultFromProcess(binary: Path, args: List<String>, process: EelProcess): Result<T, Pair<EelProcessExecutionResult, CustomErrorMessage?>>
|
||||
suspend fun getResultFromProcess(binary: BinaryToExec, args: List<String>, process: Process): Result<T, Pair<EelProcessExecutionResult, CustomErrorMessage?>>
|
||||
}
|
||||
|
||||
|
||||
@@ -76,18 +72,3 @@ typealias ProcessSemiInteractiveFun<T> = suspend (EelSendChannel, Deferred<EelPr
|
||||
* So, you can only *write* something to process.
|
||||
*/
|
||||
fun <T> processSemiInteractiveHandler(pyProcessListener: PyProcessListener? = null, code: ProcessSemiInteractiveFun<T>): ProcessInteractiveHandler<T> = ProcessSemiInteractiveHandlerImpl(pyProcessListener, code)
|
||||
|
||||
/**
|
||||
* ```kotlin
|
||||
* addLocalFile(helper)
|
||||
* addTextArgs("-v")
|
||||
* ```
|
||||
*/
|
||||
interface ArgsBuilder {
|
||||
fun addArgs(vararg args: String)
|
||||
|
||||
/**
|
||||
* This file will be copied to eel and its remote name will be added to the list of arguments
|
||||
*/
|
||||
suspend fun addLocalFile(localFile: Path)
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package com.intellij.python.community.execService
|
||||
|
||||
import com.intellij.execution.process.AnsiEscapeDecoder
|
||||
import com.intellij.execution.process.ProcessOutputTypes
|
||||
import com.intellij.execution.target.FullPathOnTarget
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.platform.eel.EelApi
|
||||
import com.intellij.platform.eel.getShell
|
||||
@@ -10,6 +12,7 @@ import com.intellij.platform.eel.provider.asNioPath
|
||||
import com.intellij.platform.eel.provider.utils.EelProcessExecutionResult
|
||||
import com.intellij.platform.eel.provider.utils.stdoutString
|
||||
import com.intellij.platform.util.progress.reportRawProgress
|
||||
import com.intellij.python.community.execService.impl.Arg
|
||||
import com.intellij.python.community.execService.impl.ExecServiceImpl
|
||||
import com.intellij.python.community.execService.impl.PyExecBundle
|
||||
import com.intellij.python.community.execService.impl.transformerToHandler
|
||||
@@ -19,6 +22,7 @@ import com.jetbrains.python.errorProcessing.PyResult
|
||||
import org.jetbrains.annotations.CheckReturnValue
|
||||
import org.jetbrains.annotations.Nls
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
@@ -29,6 +33,24 @@ import kotlin.time.Duration.Companion.minutes
|
||||
fun ExecService(): ExecService = ExecServiceImpl
|
||||
|
||||
|
||||
/**
|
||||
* There are two ways to execute binary:
|
||||
*/
|
||||
sealed interface BinaryToExec
|
||||
|
||||
/**
|
||||
* [path] on eel (Use it for anything but SSH).
|
||||
* [workDir] is pwd. As it should be on the same eel as [path] for most cases (except WSL), it is better not to set it at all.
|
||||
* Prefer full [path] over relative.
|
||||
*/
|
||||
data class BinOnEel(val path: Path, val workDir: Path? = null) : BinaryToExec
|
||||
|
||||
/**
|
||||
* Legacy Targets-based approach. Do not use it, unless you know what you are doing
|
||||
*/
|
||||
data class BinOnTarget(val path: FullPathOnTarget, val target: TargetEnvironmentConfiguration) : BinaryToExec
|
||||
|
||||
|
||||
/**
|
||||
* Execute [binary] right directly on the eel it resides on.
|
||||
*/
|
||||
@@ -37,6 +59,17 @@ suspend fun ExecService.execGetStdout(
|
||||
args: List<String> = emptyList(),
|
||||
options: ExecOptions = ExecOptions(),
|
||||
procListener: PyProcessListener? = null,
|
||||
): PyResult<String> = execGetStdout(BinOnEel(binary), args, options, procListener)
|
||||
|
||||
|
||||
/**
|
||||
* Execute [binary] right directly where it sits
|
||||
*/
|
||||
suspend fun ExecService.execGetStdout(
|
||||
binary: BinaryToExec,
|
||||
args: List<String> = emptyList(),
|
||||
options: ExecOptions = ExecOptions(),
|
||||
procListener: PyProcessListener? = null,
|
||||
): PyResult<String> = execute(
|
||||
binary = binary,
|
||||
args = args,
|
||||
@@ -59,7 +92,7 @@ suspend fun ExecService.execGetStdout(
|
||||
): PyResult<String> {
|
||||
val binary = eelApi.exec.findExeFilesInPath(binaryName).firstOrNull()?.asNioPath()
|
||||
?: return PyResult.localizedError(PyExecBundle.message("py.exec.fileNotFound", binaryName, eelApi.descriptor.machine.name))
|
||||
return execGetStdout(binary, args, options, procListener)
|
||||
return execGetStdout(BinOnEel(binary), args, options, procListener)
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +108,7 @@ suspend fun ExecService.execGetStdoutInShell(
|
||||
procListener: PyProcessListener? = null,
|
||||
): PyResult<String> {
|
||||
val (shell, arg) = eelApi.exec.getShell()
|
||||
return execGetStdout(shell.asNioPath(), listOf(arg, commandForShell) + args, options, procListener)
|
||||
return execGetStdout(BinOnEel(shell.asNioPath()), listOf(arg, commandForShell) + args, options, procListener)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,7 +121,7 @@ suspend fun ExecService.execGetStdoutInShell(
|
||||
*/
|
||||
@CheckReturnValue
|
||||
suspend fun <T> ExecService.execute(
|
||||
binary: Path,
|
||||
binary: BinaryToExec,
|
||||
args: List<String> = emptyList(),
|
||||
options: ExecOptions = ExecOptions(),
|
||||
procListener: PyProcessListener? = null,
|
||||
@@ -111,8 +144,8 @@ suspend fun <T> ExecService.execute(
|
||||
}
|
||||
}
|
||||
}
|
||||
executeAdvanced(binary, { addArgs(*args.toTypedArray()) }, options, transformerToHandler(procListener
|
||||
?: listener, processOutputTransformer))
|
||||
executeAdvanced(binary, Args(*args.toTypedArray()), options, transformerToHandler(procListener
|
||||
?: listener, processOutputTransformer))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -130,14 +163,65 @@ object ZeroCodeStdoutTransformer : ProcessOutputTransformer<String> {
|
||||
|
||||
|
||||
/**
|
||||
* @property[workingDirectory] Directory where to run the process (PWD)
|
||||
* @property[env] Environment variables to be applied with the process run
|
||||
* @property[timeout] Process gets killed after this timeout
|
||||
* @property[processDescription] optional description to be displayed to user
|
||||
*/
|
||||
data class ExecOptions(
|
||||
val env: Map<String, String> = emptyMap(),
|
||||
val workingDirectory: Path? = null,
|
||||
val processDescription: @Nls String? = null,
|
||||
val timeout: Duration = 5.minutes,
|
||||
)
|
||||
|
||||
/**
|
||||
* See [Args.addLocalFile]
|
||||
*/
|
||||
fun interface FileArgGenerator {
|
||||
fun generateArg(remoteFile: String): String
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ```kotlin
|
||||
* val args = Args()
|
||||
* args.addLocalFile(helper)
|
||||
* args.addTextArgs("-v")
|
||||
* ```
|
||||
*/
|
||||
class Args(vararg initialArgs: String) {
|
||||
private val _args = CopyOnWriteArrayList<Arg>(initialArgs.map { Arg.StringArg(it) })
|
||||
fun addArgs(vararg args: String) {
|
||||
_args.addAll(args.map { Arg.StringArg(it) })
|
||||
}
|
||||
|
||||
/**
|
||||
* This file will be copied to remote machine and its remote name will be added to the list of arguments.
|
||||
* Use [argGenerator] to modify name
|
||||
*/
|
||||
fun addLocalFile(localFile: Path, argGenerator: FileArgGenerator = FileArgGenerator { it }) {
|
||||
_args.add(Arg.FileArg(localFile, argGenerator))
|
||||
}
|
||||
|
||||
operator fun plus(second: Args): Args {
|
||||
val new = Args()
|
||||
new._args.addAll(_args)
|
||||
new._args.addAll(second._args)
|
||||
return new
|
||||
}
|
||||
|
||||
internal val localFiles: List<Path>
|
||||
get() = _args.mapNotNull {
|
||||
when (it) {
|
||||
is Arg.FileArg -> it.file
|
||||
is Arg.StringArg -> null
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun getArgs(mapFileToRemote: suspend (local: Path) -> String): List<String> =
|
||||
_args.map {
|
||||
when (it) {
|
||||
is Arg.StringArg -> it.arg
|
||||
is Arg.FileArg -> it.generator.generateArg(mapFileToRemote(it.file))
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
-106
@@ -2,142 +2,80 @@
|
||||
package com.intellij.python.community.execService.impl
|
||||
|
||||
import com.intellij.openapi.diagnostic.fileLogger
|
||||
import com.intellij.platform.eel.EelApi
|
||||
import com.intellij.platform.eel.EelProcess
|
||||
import com.intellij.platform.eel.ExecuteProcessException
|
||||
import com.intellij.platform.eel.path.EelPath
|
||||
import com.intellij.platform.eel.provider.asEelPath
|
||||
import com.intellij.platform.eel.provider.getEelDescriptor
|
||||
import com.intellij.platform.eel.provider.utils.EelPathUtils
|
||||
import com.intellij.platform.eel.spawnProcess
|
||||
import com.intellij.python.community.execService.ArgsBuilder
|
||||
import com.intellij.python.community.execService.ExecOptions
|
||||
import com.intellij.python.community.execService.ExecService
|
||||
import com.intellij.python.community.execService.ProcessInteractiveHandler
|
||||
import com.intellij.python.community.execService.*
|
||||
import com.intellij.python.community.execService.impl.processLaunchers.*
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.*
|
||||
import kotlinx.coroutines.*
|
||||
import org.jetbrains.annotations.CheckReturnValue
|
||||
import com.jetbrains.python.errorProcessing.ExecError
|
||||
import com.jetbrains.python.errorProcessing.ExecErrorReason
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.errorProcessing.failure
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.jetbrains.annotations.Nls
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.pathString
|
||||
import kotlin.time.Duration
|
||||
|
||||
|
||||
internal object ExecServiceImpl : ExecService {
|
||||
|
||||
override suspend fun <T> executeAdvanced(binary: Path, argsBuilder: suspend ArgsBuilder.() -> Unit, options: ExecOptions, processInteractiveHandler: ProcessInteractiveHandler<T>): PyExecResult<T> {
|
||||
val args = ArgsBuilderImpl(binary.getEelDescriptor().toEelApi()).apply { argsBuilder() }.args
|
||||
val description = options.processDescription
|
||||
?: PyExecBundle.message("py.exec.defaultName.process", (listOf(binary.pathString) + args).joinToString(" "))
|
||||
override suspend fun <T> executeAdvanced(binary: BinaryToExec, args: Args, options: ExecOptions, processInteractiveHandler: ProcessInteractiveHandler<T>): PyResult<T> {
|
||||
|
||||
return coroutineScope {
|
||||
val request = LaunchRequest(this, args, options.env)
|
||||
val processLauncher: ProcessLauncher = when (binary) {
|
||||
is BinOnEel -> createProcessLauncherOnEel(binary, request)
|
||||
is BinOnTarget -> createProcessLauncherOnTarget(binary, request).getOr { return@coroutineScope it }
|
||||
}
|
||||
|
||||
val description = options.processDescription
|
||||
?: PyExecBundle.message("py.exec.defaultName.process", (listOf(processLauncher.exeForError.toString()) + processLauncher.args).joinToString(" "))
|
||||
val process = processLauncher.start().getOr {
|
||||
val message = PyExecBundle.message("py.exec.start.error", description, it.error.cantExecProcessError, it.error.errNo
|
||||
?: "unknown")
|
||||
return@coroutineScope processLauncher.createExecError(
|
||||
messageToUser = message,
|
||||
errorReason = it.error
|
||||
)
|
||||
}
|
||||
|
||||
val binary = if (binary.isAbsolute) binary else options.workingDirectory?.resolve(binary) ?: binary.toAbsolutePath()
|
||||
val eelPath = binary.asEelPath()
|
||||
val executableProcess = EelExecutableProcess(eelPath, args, options.env, options.workingDirectory, description)
|
||||
val eelProcess = executableProcess.run(this).getOr { return@coroutineScope it }
|
||||
val result = try {
|
||||
withTimeout(options.timeout) {
|
||||
val interactiveResult = processInteractiveHandler.getResultFromProcess(binary, args, eelProcess)
|
||||
val interactiveResult = processInteractiveHandler.getResultFromProcess(binary, processLauncher.args, process)
|
||||
|
||||
val successResult = interactiveResult.getOr { failure ->
|
||||
val (output, customErrorMessage) = failure.error
|
||||
return@withTimeout executableProcess.failAsExecutionFailed(ExecErrorReason.UnexpectedProcessTermination(output), customErrorMessage)
|
||||
val additionalMessage = customErrorMessage ?: run {
|
||||
PyExecBundle.message("py.exec.exitCode.error", description, output.exitCode)
|
||||
}
|
||||
return@withTimeout processLauncher.createExecError(
|
||||
messageToUser = additionalMessage,
|
||||
errorReason = ExecErrorReason.UnexpectedProcessTermination(output)
|
||||
)
|
||||
}
|
||||
Result.success(successResult)
|
||||
}
|
||||
}
|
||||
catch (_: TimeoutCancellationException) {
|
||||
executableProcess.killProcessAndFailAsTimeout(eelProcess, options.timeout)
|
||||
processLauncher.killAndJoin()
|
||||
processLauncher.createExecError(
|
||||
messageToUser = PyExecBundle.message("py.exec.timeout.error", description, options.timeout),
|
||||
errorReason = ExecErrorReason.Timeout
|
||||
)
|
||||
}
|
||||
|
||||
return@coroutineScope result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class EelExecutableProcess(
|
||||
val exe: EelPath,
|
||||
val args: List<String>,
|
||||
val env: Map<String, String>,
|
||||
val workingDirectory: Path?,
|
||||
val description: @Nls String,
|
||||
)
|
||||
|
||||
@CheckReturnValue
|
||||
private suspend fun EelExecutableProcess.run(scopeToBound: CoroutineScope): PyExecResult<EelProcess> {
|
||||
val workingDirectory = if (workingDirectory != null && !workingDirectory.isAbsolute) workingDirectory.toRealPath() else workingDirectory
|
||||
try {
|
||||
val executionResult = exe.descriptor.toEelApi().exec.spawnProcess(exe.toString())
|
||||
.scope(scopeToBound)
|
||||
.args(args)
|
||||
.env(env)
|
||||
.workingDirectory(workingDirectory?.asEelPath())
|
||||
.eelIt()
|
||||
|
||||
return Result.success(executionResult)
|
||||
}
|
||||
catch (e: ExecuteProcessException) {
|
||||
return failAsCantStart(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun EelExecutableProcess.failAsCantStart(executeProcessError: ExecuteProcessException): Result.Failure<ExecError> {
|
||||
return ExecError(
|
||||
exe = Exe.OnEel(exe),
|
||||
private fun ProcessLauncher.createExecError(messageToUser: @Nls String, errorReason: ExecErrorReason): Result.Failure<ExecError> =
|
||||
ExecError(
|
||||
exe = exeForError,
|
||||
args = args.toTypedArray(),
|
||||
additionalMessageToUser = PyExecBundle.message("py.exec.start.error", description, executeProcessError.message, executeProcessError.errno),
|
||||
errorReason = ExecErrorReason.CantStart(executeProcessError.errno, executeProcessError.message)
|
||||
additionalMessageToUser = messageToUser,
|
||||
errorReason = errorReason
|
||||
).logAndFail()
|
||||
}
|
||||
|
||||
private suspend fun EelExecutableProcess.killProcessAndFailAsTimeout(eelProcess: EelProcess, timeout: Duration): Result.Failure<ExecError> {
|
||||
eelProcess.interrupt()
|
||||
eelProcess.kill()
|
||||
eelProcess.exitCode.await()
|
||||
|
||||
return ExecError(
|
||||
exe = Exe.OnEel(exe),
|
||||
args = args.toTypedArray(),
|
||||
additionalMessageToUser = PyExecBundle.message("py.exec.timeout.error", description, timeout),
|
||||
errorReason = ExecErrorReason.Timeout
|
||||
).logAndFail()
|
||||
}
|
||||
|
||||
private fun EelExecutableProcess.failAsExecutionFailed(processOutput: ExecErrorReason.UnexpectedProcessTermination, customMessage: @Nls String?): Result.Failure<ExecError> {
|
||||
val additionalMessage = customMessage ?: run {
|
||||
PyExecBundle.message("py.exec.exitCode.error", description, processOutput.exitCode)
|
||||
}
|
||||
|
||||
return ExecError(
|
||||
exe = Exe.OnEel(exe),
|
||||
args = args.toTypedArray(),
|
||||
additionalMessageToUser = additionalMessage,
|
||||
errorReason = processOutput
|
||||
).logAndFail()
|
||||
}
|
||||
|
||||
private fun ExecError.logAndFail(): Result.Failure<ExecError> {
|
||||
fileLogger().warn(message)
|
||||
return failure(this)
|
||||
}
|
||||
|
||||
private class ArgsBuilderImpl(private val eel: EelApi) : ArgsBuilder {
|
||||
private val _args = CopyOnWriteArrayList<String>()
|
||||
val args: List<String> = _args
|
||||
override fun addArgs(vararg args: String) {
|
||||
_args.addAll(args)
|
||||
}
|
||||
|
||||
override suspend fun addLocalFile(localFile: Path): Unit = withContext(Dispatchers.IO) {
|
||||
assert(localFile.exists()) { "No file $localFile, be sure to check it before calling" }
|
||||
val remoteFile = EelPathUtils.transferLocalContentToRemote(
|
||||
source = localFile,
|
||||
target = EelPathUtils.TransferTarget.Temporary(eel.descriptor)
|
||||
).asEelPath().toString()
|
||||
_args.add(remoteFile)
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.community.execService.impl
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
|
||||
internal sealed interface Arg {
|
||||
data class StringArg(val arg: String) : Arg
|
||||
data class FileArg(val file: Path, val generator: com.intellij.python.community.execService.FileArgGenerator) : Arg
|
||||
}
|
||||
+5
-5
@@ -1,25 +1,25 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.community.execService.impl
|
||||
|
||||
import com.intellij.platform.eel.EelProcess
|
||||
import com.intellij.platform.eel.provider.utils.EelProcessExecutionResult
|
||||
import com.intellij.platform.eel.provider.utils.asEelChannel
|
||||
import com.intellij.python.community.execService.*
|
||||
import com.intellij.util.io.awaitExit
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.mapError
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import java.nio.file.Path
|
||||
|
||||
internal class ProcessSemiInteractiveHandlerImpl<T>(
|
||||
private val pyProcessListener: PyProcessListener?,
|
||||
private val code: ProcessSemiInteractiveFun<T>,
|
||||
) : ProcessInteractiveHandler<T> {
|
||||
override suspend fun getResultFromProcess(binary: Path, args: List<String>, process: EelProcess): Result<T, Pair<EelProcessExecutionResult, CustomErrorMessage?>> =
|
||||
override suspend fun getResultFromProcess(binary: BinaryToExec, args: List<String>, process: Process): Result<T, Pair<EelProcessExecutionResult, CustomErrorMessage?>> =
|
||||
coroutineScope {
|
||||
pyProcessListener?.emit(ProcessEvent.ProcessStarted(binary, args))
|
||||
val processOutput = async { process.awaitWithReporting(pyProcessListener) }
|
||||
val result = code(process.stdin, processOutput)
|
||||
pyProcessListener?.emit(ProcessEvent.ProcessEnded(process.exitCode.await()))
|
||||
val result = code(process.outputStream.asEelChannel(), processOutput)
|
||||
pyProcessListener?.emit(ProcessEvent.ProcessEnded(process.awaitExit()))
|
||||
return@coroutineScope result.mapError { customErrorMessage ->
|
||||
Pair(processOutput.await(), customErrorMessage)
|
||||
}
|
||||
|
||||
+11
-10
@@ -1,36 +1,37 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.community.execService.impl
|
||||
|
||||
import com.intellij.platform.eel.EelProcess
|
||||
import com.intellij.platform.eel.provider.utils.EelProcessExecutionResult
|
||||
import com.intellij.platform.eel.provider.utils.consumeAsInputStream
|
||||
import com.intellij.python.community.execService.ProcessEvent
|
||||
import com.intellij.python.community.execService.ProcessEvent.OutputType
|
||||
import com.intellij.python.community.execService.ProcessEvent.OutputType.STDERR
|
||||
import com.intellij.python.community.execService.ProcessEvent.OutputType.STDOUT
|
||||
import kotlinx.coroutines.*
|
||||
import com.intellij.util.io.awaitExit
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Awaits of process result and reports its stdout/stderr as a progress.
|
||||
*/
|
||||
internal suspend fun EelProcess.awaitWithReporting(progressListener: FlowCollector<ProcessEvent.ProcessOutput>?): EelProcessExecutionResult =
|
||||
internal suspend fun Process.awaitWithReporting(progressListener: FlowCollector<ProcessEvent.ProcessOutput>?): EelProcessExecutionResult =
|
||||
coroutineScope {
|
||||
val stdout = async { report(STDOUT, progressListener) }
|
||||
val stderr = async { report(STDERR, progressListener) }
|
||||
EelProcessExecutionResult(exitCode.await(), stdout = stdout.await(), stderr = stderr.await())
|
||||
EelProcessExecutionResult(awaitExit(), stdout = stdout.await(), stderr = stderr.await())
|
||||
}
|
||||
|
||||
private suspend fun EelProcess.report(outputType: OutputType, to: FlowCollector<ProcessEvent.ProcessOutput>?): ByteArray = withContext(Dispatchers.IO) {
|
||||
private suspend fun Process.report(outputType: OutputType, to: FlowCollector<ProcessEvent.ProcessOutput>?): ByteArray = withContext(Dispatchers.IO) {
|
||||
val from = when (outputType) {
|
||||
STDOUT -> stdout
|
||||
STDERR -> stderr
|
||||
STDOUT -> inputStream
|
||||
STDERR -> errorStream
|
||||
}
|
||||
val result = StringBuilder()
|
||||
|
||||
|
||||
from.consumeAsInputStream().use { inputStream ->
|
||||
from.use { inputStream ->
|
||||
val reader = inputStream.reader()
|
||||
|
||||
val currentLine = StringBuilder()
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.community.execService.impl.processLaunchers
|
||||
|
||||
import com.intellij.openapi.diagnostic.fileLogger
|
||||
import com.intellij.platform.eel.provider.utils.ProcessFunctions
|
||||
import com.intellij.python.community.execService.Args
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.Exe
|
||||
import com.jetbrains.python.errorProcessing.ExecErrorReason
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
private val logger = fileLogger()
|
||||
|
||||
internal class ProcessLauncher(
|
||||
val exeForError: Exe,
|
||||
val args: List<String>,
|
||||
private val processCommands: ProcessCommands,
|
||||
) {
|
||||
suspend fun start(): Result<Process, ExecErrorReason.CantStart> = processCommands.start()
|
||||
suspend fun killAndJoin() {
|
||||
processCommands.processFunctions.killAndJoin(logger, exeForError.toString())
|
||||
}
|
||||
}
|
||||
|
||||
internal interface ProcessCommands {
|
||||
suspend fun start(): Result<Process, ExecErrorReason.CantStart>
|
||||
val processFunctions: ProcessFunctions
|
||||
}
|
||||
|
||||
internal data class LaunchRequest(
|
||||
val scopeToBind: CoroutineScope,
|
||||
val args: Args,
|
||||
val env: Map<String, String>,
|
||||
)
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.community.execService.impl.processLaunchers
|
||||
|
||||
import com.intellij.platform.eel.EelProcess
|
||||
import com.intellij.platform.eel.ExecuteProcessException
|
||||
import com.intellij.platform.eel.path.EelPath
|
||||
import com.intellij.platform.eel.provider.asEelPath
|
||||
import com.intellij.platform.eel.provider.utils.EelPathUtils
|
||||
import com.intellij.platform.eel.provider.utils.ProcessFunctions
|
||||
import com.intellij.platform.eel.spawnProcess
|
||||
import com.intellij.python.community.execService.BinOnEel
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.Exe
|
||||
import com.jetbrains.python.errorProcessing.ExecErrorReason
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
internal suspend fun createProcessLauncherOnEel(binOnEel: BinOnEel, launchRequest: LaunchRequest): ProcessLauncher {
|
||||
val exePath: EelPath = with(binOnEel) {
|
||||
(if (path.isAbsolute) path else workDir?.resolve(binOnEel.path) ?: path.toAbsolutePath()).asEelPath()
|
||||
}
|
||||
val eel = exePath.descriptor.toEelApi()
|
||||
val args = launchRequest.args.getArgs { file ->
|
||||
EelPathUtils.transferLocalContentToRemote(
|
||||
source = file,
|
||||
target = EelPathUtils.TransferTarget.Temporary(eel.descriptor)
|
||||
).asEelPath().toString()
|
||||
}
|
||||
return ProcessLauncher(
|
||||
exeForError = Exe.OnEel(exePath),
|
||||
args = args,
|
||||
processCommands = EelProcessCommands(launchRequest.scopeToBind, binOnEel, exePath, args, launchRequest.env)
|
||||
)
|
||||
}
|
||||
|
||||
private class EelProcessCommands(
|
||||
private val scopeToBind: CoroutineScope,
|
||||
private val binOnEel: BinOnEel,
|
||||
private val path: EelPath,
|
||||
private val args: List<String>,
|
||||
private val env: Map<String, String>,
|
||||
) : ProcessCommands {
|
||||
private var eelProcess: EelProcess? = null
|
||||
|
||||
override val processFunctions: ProcessFunctions = ProcessFunctions(
|
||||
waitForExit = { eelProcess?.exitCode?.await() },
|
||||
killProcess = { eelProcess?.kill() }
|
||||
)
|
||||
|
||||
|
||||
override suspend fun start(): Result<Process, ExecErrorReason.CantStart> {
|
||||
var workDir = binOnEel.workDir
|
||||
workDir = if (workDir != null && !workDir.isAbsolute) workDir.toRealPath() else workDir
|
||||
try {
|
||||
val eelProcess = path.descriptor.toEelApi().exec.spawnProcess(path)
|
||||
.scope(scopeToBind)
|
||||
.args(args)
|
||||
.env(env)
|
||||
.workingDirectory(workDir?.asEelPath())
|
||||
.eelIt()
|
||||
this.eelProcess = eelProcess
|
||||
return Result.success(eelProcess.convertToJavaProcess())
|
||||
}
|
||||
catch (e: ExecuteProcessException) {
|
||||
return Result.failure(ExecErrorReason.CantStart(e.errno, e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
/**
|
||||
* There are two launchers: Eel (modern) and targets (legacy)
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
package com.intellij.python.community.execService.impl.processLaunchers;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
@file:Suppress("UsePlatformProcessAwaitExit")
|
||||
|
||||
package com.intellij.python.community.execService.impl.processLaunchers
|
||||
|
||||
import com.intellij.execution.ExecutionException
|
||||
import com.intellij.execution.target.*
|
||||
import com.intellij.openapi.diagnostic.fileLogger
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import com.intellij.platform.eel.provider.utils.ProcessFunctions
|
||||
import com.intellij.platform.eel.provider.utils.bindProcessToScopeImpl
|
||||
import com.intellij.python.community.execService.BinOnTarget
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.Exe
|
||||
import com.jetbrains.python.errorProcessing.ExecErrorReason
|
||||
import com.jetbrains.python.errorProcessing.MessageError
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.io.path.pathString
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
private val logger = fileLogger()
|
||||
|
||||
internal suspend fun createProcessLauncherOnTarget(binOnTarget: BinOnTarget, launchRequest: LaunchRequest): PyResult<ProcessLauncher> = withContext(Dispatchers.IO) {
|
||||
val target = binOnTarget.target
|
||||
val projectMan = ProjectManager.getInstance()
|
||||
// Broken Targets API doesn't work without project
|
||||
val request = target.createEnvironmentRequest(projectMan.openProjects.firstOrNull() ?: projectMan.defaultProject)
|
||||
// Broken Targets API can only upload the whole directory
|
||||
val dirsToMap = launchRequest.args.localFiles.map { it.parent }.toSet()
|
||||
for (localDir in dirsToMap) {
|
||||
request.uploadVolumes.add(TargetEnvironment.UploadRoot(localDir, TargetEnvironment.TargetPath.Temporary(), removeAtShutdown = true))
|
||||
}
|
||||
val targetEnv = try {
|
||||
request.prepareEnvironment(TargetProgressIndicator.EMPTY)
|
||||
}
|
||||
catch (e: ExecutionException) {
|
||||
fileLogger().warn("Failed to start $target", e)
|
||||
// TODO: i18n
|
||||
return@withContext Result.failure(MessageError("Failed to start environment due to ${e.localizedMessage}"))
|
||||
}
|
||||
val args = launchRequest.args.getArgs { localFile ->
|
||||
targetEnv.getTargetPaths(localFile.pathString).first()
|
||||
}
|
||||
return@withContext Result.success(
|
||||
ProcessLauncher(
|
||||
exeForError = Exe.OnTarget(binOnTarget.path),
|
||||
args = args,
|
||||
processCommands = TargetProcessCommands(launchRequest.scopeToBind, binOnTarget.path, request, targetEnv, args, launchRequest.env)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private class TargetProcessCommands(
|
||||
private val scopeToBind: CoroutineScope,
|
||||
private val exePath: FullPathOnTarget,
|
||||
private val request: TargetEnvironmentRequest,
|
||||
private val targetEnv: TargetEnvironment,
|
||||
private val args: List<String>,
|
||||
private val env: Map<String, String>,
|
||||
) : ProcessCommands {
|
||||
|
||||
private var process: Process? = null
|
||||
|
||||
override val processFunctions: ProcessFunctions = ProcessFunctions(
|
||||
waitForExit = {
|
||||
// `waitForExit` seems to be broken in Targets API, hence polling
|
||||
while (process?.isAlive == true) {
|
||||
delay(100.milliseconds)
|
||||
}
|
||||
},
|
||||
killProcess = { process?.destroyForcibly() }
|
||||
)
|
||||
|
||||
override suspend fun start(): Result<Process, ExecErrorReason.CantStart> {
|
||||
val cmdLine = TargetedCommandLineBuilder(request).also {
|
||||
it.setExePath(exePath)
|
||||
it.addParameters(args)
|
||||
for ((k, v) in env) {
|
||||
it.addEnvironmentVariable(k, v)
|
||||
}
|
||||
}.build()
|
||||
try {
|
||||
val process = targetEnv.createProcess(cmdLine)
|
||||
this.process = process
|
||||
scopeToBind.bindProcessToScopeImpl(
|
||||
logger = logger,
|
||||
processNameForDebug = exePath,
|
||||
processFunctions = processFunctions
|
||||
)
|
||||
return Result.success(process)
|
||||
}
|
||||
catch (e: ExecutionException) {
|
||||
return e.asCantStart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ExecutionException.asCantStart(): Result.Failure<ExecErrorReason.CantStart> = Result.failure(ExecErrorReason.CantStart(null, localizedMessage))
|
||||
+44
-18
@@ -4,9 +4,7 @@ package com.intellij.python.junit5Tests.unit.alsoWin
|
||||
import com.intellij.platform.eel.EelPlatform
|
||||
import com.intellij.platform.eel.getShell
|
||||
import com.intellij.platform.eel.provider.asNioPath
|
||||
import com.intellij.platform.eel.provider.utils.readWholeText
|
||||
import com.intellij.platform.eel.provider.utils.sendWholeText
|
||||
import com.intellij.platform.eel.provider.utils.stdoutString
|
||||
import com.intellij.platform.eel.provider.utils.*
|
||||
import com.intellij.platform.testFramework.junit5.eel.params.api.EelHolder
|
||||
import com.intellij.platform.testFramework.junit5.eel.params.api.EelSource
|
||||
import com.intellij.platform.testFramework.junit5.eel.params.api.TestApplicationWithEel
|
||||
@@ -15,6 +13,7 @@ import com.intellij.testFramework.common.timeoutRunBlocking
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.Exe
|
||||
import com.jetbrains.python.errorProcessing.ExecError
|
||||
import com.jetbrains.python.errorProcessing.MessageError
|
||||
import com.jetbrains.python.getOrThrow
|
||||
import kotlinx.coroutines.*
|
||||
import org.hamcrest.CoreMatchers
|
||||
@@ -109,7 +108,7 @@ class ExecServiceShowCaseTest {
|
||||
val (shell, execArg) = eel.exec.getShell()
|
||||
val args = listOf(execArg, "echo Alice,25 && echo Bob,48")
|
||||
|
||||
val records = ExecService().execute((shell.asNioPath()), args) { output ->
|
||||
val records = ExecService().execute((BinOnEel(shell.asNioPath())), args) { output ->
|
||||
val stdout = output.stdoutString.trim()
|
||||
when {
|
||||
output.exitCode == 123 -> {
|
||||
@@ -159,17 +158,36 @@ class ExecServiceShowCaseTest {
|
||||
assertThat("Command doesn't have expected output", output, CoreMatchers.containsString(expectedPhrase))
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EelSource
|
||||
fun testInteractive(eelHolder: EelHolder): Unit = timeoutRunBlocking {
|
||||
@CartesianTest
|
||||
fun testInteractive(
|
||||
@EelSource eelHolder: EelHolder,
|
||||
@CartesianTest.Values(booleans = [true, false]) useEelChannels: Boolean,
|
||||
): Unit = timeoutRunBlocking(13.minutes, context = Dispatchers.IO) {
|
||||
val string = "abc123"
|
||||
val shell = eelHolder.eel.exec.getShell().first.asNioPath()
|
||||
val output = ExecService().executeAdvanced(shell, {}, processInteractiveHandler = ProcessInteractiveHandler<String> { _, _, process ->
|
||||
val stdout = async {
|
||||
process.stdout.readWholeText()
|
||||
val output = ExecService().executeAdvanced(BinOnEel(shell), Args(), processInteractiveHandler = ProcessInteractiveHandler<String> { _, _, process ->
|
||||
val stdout = this@timeoutRunBlocking.async {
|
||||
if (useEelChannels) {
|
||||
process.inputStream.consumeAsEelChannel().readWholeText()
|
||||
}
|
||||
else {
|
||||
process.inputStream.bufferedReader().readText()
|
||||
}
|
||||
}
|
||||
val commands = arrayOf("echo $string\n", "exit\n")
|
||||
if (useEelChannels) {
|
||||
val eelChannel = process.outputStream.asEelChannel()
|
||||
for (cmd in commands) {
|
||||
eelChannel.sendWholeText(cmd)
|
||||
}
|
||||
}
|
||||
else {
|
||||
val writer = process.outputStream.bufferedWriter()
|
||||
for (cmd in commands) {
|
||||
writer.write(cmd)
|
||||
writer.flush()
|
||||
}
|
||||
}
|
||||
process.stdin.sendWholeText("echo $string\n")
|
||||
process.stdin.sendWholeText("exit\n")
|
||||
Result.success(stdout.await())
|
||||
}).orThrow()
|
||||
assertThat("No expected output", output, CoreMatchers.containsString(string))
|
||||
@@ -182,7 +200,7 @@ class ExecServiceShowCaseTest {
|
||||
): Unit = timeoutRunBlocking {
|
||||
val messageToUser = "abc123"
|
||||
val shell = eelHolder.eel.exec.getShell().first.asNioPath()
|
||||
val result = ExecService().executeAdvanced(shell, {}, processInteractiveHandler = processSemiInteractiveHandler<Unit> { channel, exitCode ->
|
||||
val result = ExecService().executeAdvanced(BinOnEel(shell), Args(), processInteractiveHandler = processSemiInteractiveHandler<Unit> { channel, exitCode ->
|
||||
channel.sendWholeText("exit\n")
|
||||
assertEquals(0, exitCode.await().exitCode, "Wrong exit code")
|
||||
if (sunny) {
|
||||
@@ -194,9 +212,17 @@ class ExecServiceShowCaseTest {
|
||||
})
|
||||
when (result) {
|
||||
is Result.Failure -> {
|
||||
assertFalse(sunny, "Unexpected failure ${result.error}")
|
||||
assertThat("Wrong message to user", result.error.message, CoreMatchers.containsString(messageToUser))
|
||||
assertEquals(shell, (result.error.exe as Exe.OnEel).eelPath.asNioPath(), "Wrong exe")
|
||||
when (val err = result.error) {
|
||||
is ExecError -> {
|
||||
assertFalse(sunny, "Unexpected failure ${result.error}")
|
||||
assertThat("Wrong message to user",
|
||||
result.error.message, CoreMatchers.containsString(messageToUser))
|
||||
assertEquals(shell, (err.exe as Exe.OnEel).eelPath.asNioPath(), "Wrong exe")
|
||||
}
|
||||
is MessageError -> {
|
||||
fail("Unexpected error $err")
|
||||
}
|
||||
}
|
||||
}
|
||||
is Result.Success -> {
|
||||
assertTrue(sunny, "Unexpected success")
|
||||
@@ -236,7 +262,7 @@ class ExecServiceShowCaseTest {
|
||||
val progressCapturer = PyProcessListener { event ->
|
||||
when (event) {
|
||||
is ProcessEvent.ProcessStarted -> {
|
||||
assertEquals(shell, event.binary, "Wrong args for start event")
|
||||
assertEquals(shell, (event.binary as BinOnEel).path, "Wrong args for start event")
|
||||
processStartEvent = true
|
||||
}
|
||||
is ProcessEvent.ProcessOutput -> {
|
||||
@@ -248,7 +274,7 @@ class ExecServiceShowCaseTest {
|
||||
}
|
||||
}
|
||||
|
||||
ExecService().executeAdvanced(shell, argsBuilder = {}, processInteractiveHandler = processSemiInteractiveHandler<Unit>(progressCapturer) { stdin, _ ->
|
||||
ExecService().executeAdvanced(BinOnEel(shell), args = Args(), processInteractiveHandler = processSemiInteractiveHandler<Unit>(progressCapturer) { stdin, _ ->
|
||||
for (string in text) {
|
||||
stdin.sendWholeText("echo $string\n")
|
||||
delay(500)
|
||||
|
||||
@@ -16,7 +16,7 @@ import kotlin.io.path.isExecutable
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
class HatchRuntime(
|
||||
val hatchBinary: Path,
|
||||
val hatchBinary: BinOnEel,
|
||||
val execOptions: ExecOptions,
|
||||
private val execService: ExecService = ExecService(),
|
||||
) {
|
||||
@@ -35,8 +35,8 @@ class HatchRuntime(
|
||||
}
|
||||
|
||||
val runtime = HatchRuntime(
|
||||
hatchBinary = this.hatchBinary,
|
||||
execOptions = this.execOptions.copy(workingDirectory = workDirectoryPath)
|
||||
hatchBinary = this.hatchBinary.copy(workDir = workDirectoryPath),
|
||||
execOptions = this.execOptions
|
||||
)
|
||||
return Result.success(runtime)
|
||||
}
|
||||
@@ -59,7 +59,7 @@ class HatchRuntime(
|
||||
}
|
||||
|
||||
internal suspend fun <T> executeInteractive(vararg arguments: String, processSemiInteractiveFun: ProcessSemiInteractiveFun<T>): PyResult<T> {
|
||||
return execService.executeAdvanced(hatchBinary, { addArgs(*arguments) }, execOptions, processSemiInteractiveHandler(code = processSemiInteractiveFun))
|
||||
return execService.executeAdvanced(hatchBinary, Args(*arguments), execOptions, processSemiInteractiveHandler(code = processSemiInteractiveFun))
|
||||
}
|
||||
|
||||
internal suspend fun resolvePythonVirtualEnvironment(pythonHomePath: PythonHomePath): PyResult<PythonVirtualEnvironment> {
|
||||
@@ -101,10 +101,9 @@ suspend fun createHatchRuntime(
|
||||
val actualEnvVars = defaultVariables + envVars
|
||||
|
||||
val runtime = HatchRuntime(
|
||||
hatchBinary = actualHatchExecutable,
|
||||
hatchBinary = BinOnEel(actualHatchExecutable, workingDirectoryPath),
|
||||
execOptions = ExecOptions(
|
||||
env = actualEnvVars,
|
||||
workingDirectory = workingDirectoryPath
|
||||
)
|
||||
)
|
||||
return Result.success(runtime)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk
|
||||
|
||||
import com.intellij.python.community.execService.BinOnEel
|
||||
import com.intellij.python.community.execService.ExecOptions
|
||||
import com.intellij.python.community.execService.ExecService
|
||||
import com.intellij.python.community.execService.execGetStdout
|
||||
@@ -28,6 +29,6 @@ suspend fun runExecutableWithProgress(
|
||||
env: Map<String, String> = emptyMap(),
|
||||
vararg args: String,
|
||||
): PyResult<String> {
|
||||
val execOptions = ExecOptions(workingDirectory = workDir, timeout = timeout, env = env)
|
||||
return ExecService().execGetStdout(executable, args.toList(), execOptions)
|
||||
val execOptions = ExecOptions(timeout = timeout, env = env)
|
||||
return ExecService().execGetStdout(BinOnEel(executable, workDir), args.toList(), execOptions)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.python.community.execService.ExecOptions
|
||||
import com.intellij.python.community.execService.BinOnEel
|
||||
import com.intellij.python.community.execService.ExecService
|
||||
import com.intellij.python.community.execService.execGetStdout
|
||||
import com.intellij.python.community.impl.poetry.poetryPath
|
||||
@@ -119,7 +119,7 @@ suspend fun setupPoetry(projectPath: Path, python: String?, installPackages: Boo
|
||||
.getOr { return it }
|
||||
|
||||
if (python != null) { // Replace a python version in toml
|
||||
ExecService().execGetStdout(Path.of(python), listOf("-c", REPLACE_PYTHON_VERSION), ExecOptions(workingDirectory = projectPath))
|
||||
ExecService().execGetStdout(BinOnEel(Path.of(python), workDir = projectPath), listOf("-c", REPLACE_PYTHON_VERSION))
|
||||
.getOr { return it }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user