mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-82666 Fix undetached process cancellation when running packaging tool
Pip packaging tool used legacy process handler, which led to undetached processes being spawned and even process leaks if they are really long running. This led to flaky tests that were trying to delete temporary directories after the test was finished, and at the same time process was still running and creating __pycache__ directories there. That's where a DirectoryNotEmptyException was thrown leading to a test failure. The whole problem was fixed by using a new ExecService instead of a legacy process handler. Also unused register keys `python.packaging.tool.use.project.location.as.working.dir` and `python.packaging.tool.upload.project` have been removed. Merge-request: IJ-MR-176080 Merged-by: Alexey Katsman <alexey.katsman@jetbrains.com> GitOrigin-RevId: 31c02fc8c25abd5941518958e7e2651f3771bad1
This commit is contained in:
committed by
intellij-monorepo-bot
parent
94c624aae7
commit
0c509dbb23
@@ -572,10 +572,6 @@
|
||||
|
||||
<!-- Targets API -->
|
||||
<registryKey key="enable.conda.on.targets" defaultValue="false" description="Enables Conda configuration on targets."/>
|
||||
<registryKey key="python.packaging.tool.use.project.location.as.working.dir" defaultValue="false"
|
||||
description="Use project location as a working directory for the packaging tool execution."/>
|
||||
<registryKey key="python.packaging.tool.upload.project" defaultValue="false"
|
||||
description="Upload project files and use this location as a working directory for the packaging tool execution."/>
|
||||
|
||||
<notificationGroup id="Python Debugger" displayType="TOOL_WINDOW" toolWindowId="Debug" bundle="messages.PyBundle"
|
||||
key="debug.notification.group"/>
|
||||
|
||||
@@ -185,7 +185,7 @@ open class ZeroCodeStdoutParserTransformer<T>(val stdoutParser: (String) -> Resu
|
||||
* @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
|
||||
* [tty] Much like [com.intellij.platform.eel.EelExecApi.Pty]
|
||||
* @property[tty] Much like [com.intellij.platform.eel.EelExecApi.Pty]
|
||||
*/
|
||||
data class ExecOptions(
|
||||
override val env: Map<String, String> = emptyMap(),
|
||||
|
||||
@@ -22,13 +22,14 @@ import java.util.stream.Collectors;
|
||||
import static com.intellij.python.community.helpersLocator.PythonHelpersLocator.*;
|
||||
import static com.intellij.python.community.impl.venv.VenvKt.LEGACY_VIRTUALENV_ZIPAPP_NAME;
|
||||
import static com.intellij.python.community.impl.venv.VenvKt.VIRTUALENV_ZIPAPP_NAME;
|
||||
import static com.jetbrains.python.packaging.pip.PipPackageManagerEngine.PACKAGING_TOOL_NAME;
|
||||
|
||||
public enum PythonHelper implements HelperPackage {
|
||||
GENERATOR3("generator3/__main__.py"),
|
||||
REMOTE_SYNC("remote_sync.py"),
|
||||
|
||||
// Packaging tools
|
||||
PACKAGING_TOOL("packaging_tool.py"),
|
||||
PACKAGING_TOOL(PACKAGING_TOOL_NAME),
|
||||
VIRTUALENV_ZIPAPP(VIRTUALENV_ZIPAPP_NAME),
|
||||
LEGACY_VIRTUALENV_ZIPAPP(LEGACY_VIRTUALENV_ZIPAPP_NAME), // virtualenv used to create virtual environments for python 2.7, 3.6, 3.7
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// 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.packaging.management
|
||||
|
||||
import com.intellij.execution.process.CapturingAnsiEscapesAwareProcessHandler
|
||||
import com.intellij.execution.process.CapturingProcessAdapter
|
||||
import com.intellij.execution.process.ProcessOutput
|
||||
import com.intellij.execution.process.ProcessOutputTypes
|
||||
import com.intellij.openapi.util.Key
|
||||
|
||||
internal class PyPackageProcessHandler : CapturingAnsiEscapesAwareProcessHandler {
|
||||
constructor(process: Process, commandLine: String) : super(process, commandLine)
|
||||
|
||||
var lastLine: String = ""
|
||||
private set
|
||||
|
||||
var lastLineNotifier: ((String) -> Unit)? = null
|
||||
|
||||
override fun createProcessAdapter(processOutput: ProcessOutput): CapturingProcessAdapter? {
|
||||
return PackageProcessAdapter(processOutput)
|
||||
}
|
||||
|
||||
private inner class PackageProcessAdapter(processOutput: ProcessOutput) : AnsiEscapesAwareAdapter(processOutput) {
|
||||
override fun addToOutput(text: String, outputType: Key<*>) {
|
||||
super.addToOutput(text, outputType)
|
||||
|
||||
when {
|
||||
outputType === ProcessOutputTypes.STDOUT -> {
|
||||
lastLine = output.stdoutLines.lastOrNull { it.isNotBlank() } ?: ""
|
||||
|
||||
}
|
||||
outputType === ProcessOutputTypes.STDERR -> {
|
||||
lastLine = output.stderrLines.lastOrNull { it.isNotBlank() } ?: ""
|
||||
}
|
||||
}
|
||||
lastLine = lastLine.trim()
|
||||
if (lastLine.isBlank())
|
||||
return
|
||||
lastLineNotifier?.invoke(lastLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// 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.packaging.management
|
||||
|
||||
import com.intellij.execution.process.ProcessOutput
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.progress.coroutineToIndicator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
|
||||
@ApiStatus.Internal
|
||||
internal object PythonPackageManagerRunner {
|
||||
suspend fun runProcess(
|
||||
process: Process,
|
||||
command: String,
|
||||
): ProcessOutput {
|
||||
val handler = PyPackageProcessHandler(process, command)
|
||||
|
||||
return runProcessInternal(handler)
|
||||
}
|
||||
|
||||
private suspend fun runProcessInternal(processHandler: PyPackageProcessHandler): ProcessOutput = withContext(Dispatchers.IO) {
|
||||
coroutineToIndicator {
|
||||
val progressIndicator = ProgressManager.getInstance().progressIndicator
|
||||
processHandler.lastLineNotifier = { line: String ->
|
||||
@Suppress("HardCodedStringLiteral")
|
||||
progressIndicator.text = line.trim()
|
||||
}
|
||||
processHandler.runProcessWithProgressIndicator(progressIndicator, 10 * 60 * 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,14 @@
|
||||
// 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.packaging.pip
|
||||
|
||||
import com.intellij.execution.target.TargetProgressIndicator
|
||||
import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
|
||||
import com.intellij.execution.target.value.targetPath
|
||||
import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.openapi.progress.EmptyProgressIndicator
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.guessProjectDir
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.util.io.IdeUtilIoBundle
|
||||
import com.jetbrains.python.sdk.impl.PySdkBundle
|
||||
import com.jetbrains.python.PythonHelper
|
||||
import com.intellij.python.community.execService.ExecService
|
||||
import com.intellij.python.community.execService.python.HelperName
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.onFailure
|
||||
import com.jetbrains.python.packaging.PyExecutionException
|
||||
import com.jetbrains.python.packaging.PyPIPackageUtil
|
||||
import com.jetbrains.python.packaging.common.PythonOutdatedPackage
|
||||
import com.jetbrains.python.packaging.common.PythonPackage
|
||||
@@ -25,16 +16,11 @@ import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecificatio
|
||||
import com.jetbrains.python.packaging.management.PythonPackageInstallRequest
|
||||
import com.jetbrains.python.packaging.management.PythonPackageManager
|
||||
import com.jetbrains.python.packaging.management.PythonPackageManagerEngine
|
||||
import com.jetbrains.python.packaging.management.PythonPackageManagerRunner
|
||||
import com.jetbrains.python.packaging.utils.PyProxyUtils
|
||||
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
|
||||
import com.jetbrains.python.run.buildTargetedCommandLine
|
||||
import com.jetbrains.python.run.ensureProjectSdkAndModuleDirsAreOnTarget
|
||||
import com.jetbrains.python.run.prepareHelperScriptExecution
|
||||
import com.jetbrains.python.sdk.executeHelper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import kotlin.math.min
|
||||
|
||||
|
||||
@ApiStatus.Internal
|
||||
@@ -95,91 +81,22 @@ class PipPackageManagerEngine(
|
||||
return PyResult.success(packages)
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
suspend fun runPackagingTool(operation: String, arguments: List<String>): PyResult<String> = withContext(Dispatchers.IO) {
|
||||
// todo[akniazev]: check for package management tools
|
||||
val helpersAwareTargetRequest = PythonInterpreterTargetEnvironmentFactory.findPythonTargetInterpreter(sdk, project)
|
||||
val targetEnvironmentRequest = helpersAwareTargetRequest.targetEnvironmentRequest
|
||||
val pythonExecution = prepareHelperScriptExecution(PythonHelper.PACKAGING_TOOL, helpersAwareTargetRequest)
|
||||
|
||||
if (targetEnvironmentRequest is LocalTargetEnvironmentRequest) {
|
||||
if (Registry.`is`("python.packaging.tool.use.project.location.as.working.dir")) {
|
||||
project.guessProjectDir()?.toNioPath()?.let {
|
||||
pythonExecution.workingDir = targetPath(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (Registry.`is`("python.packaging.tool.upload.project")) {
|
||||
project.guessProjectDir()?.toNioPath()?.let {
|
||||
targetEnvironmentRequest.ensureProjectSdkAndModuleDirsAreOnTarget(project)
|
||||
pythonExecution.workingDir = targetPath(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pythonExecution.addParameter(operation)
|
||||
val parameters = mutableListOf(operation)
|
||||
if (operation == "install") {
|
||||
PyProxyUtils.proxyString?.let {
|
||||
pythonExecution.addParameter("--proxy")
|
||||
pythonExecution.addParameter(it)
|
||||
parameters += "--proxy"
|
||||
parameters += it
|
||||
}
|
||||
}
|
||||
|
||||
arguments.forEach(pythonExecution::addParameter)
|
||||
|
||||
// // todo[akniazev]: add extra args to package specification
|
||||
|
||||
val targetProgressIndicator = TargetProgressIndicator.EMPTY
|
||||
val targetEnvironment = targetEnvironmentRequest.prepareEnvironment(targetProgressIndicator)
|
||||
|
||||
targetEnvironment.uploadVolumes.entries.forEach { (_, value) ->
|
||||
value.upload(".", targetProgressIndicator)
|
||||
}
|
||||
|
||||
val targetedCommandLine = pythonExecution.buildTargetedCommandLine(targetEnvironment, sdk, emptyList())
|
||||
|
||||
val indicator = ProgressManager.getInstance().progressIndicator ?: EmptyProgressIndicator()
|
||||
// from targets package manager
|
||||
// TODO [targets] Apply environment variables: setPythonUnbuffered(...), setPythonDontWriteBytecode(...), resetHomePathChanges(...)
|
||||
// TODO [targets] Apply flavor from PythonSdkFlavor.getFlavor(mySdk)
|
||||
// TODO [targets] check askForSudo
|
||||
|
||||
val process = targetEnvironment.createProcess(targetedCommandLine, indicator)
|
||||
|
||||
val commandLine = targetedCommandLine.collectCommandsSynchronously()
|
||||
val commandLineString = commandLine.joinToString(" ")
|
||||
parameters += arguments
|
||||
|
||||
thisLogger().debug("Running python packaging tool. Operation: $operation")
|
||||
|
||||
val result = PythonPackageManagerRunner.runProcess(
|
||||
process,
|
||||
commandLineString
|
||||
ExecService().executeHelper(
|
||||
sdk,
|
||||
PACKAGING_TOOL_NAME,
|
||||
parameters,
|
||||
)
|
||||
if (result.isCancelled) {
|
||||
return@withContext PyResult.localizedError(IdeUtilIoBundle.message("run.canceled.by.user.message"))
|
||||
}
|
||||
|
||||
result.checkSuccess(thisLogger())
|
||||
val exitCode = result.exitCode
|
||||
val helperPath = commandLine.firstOrNull() ?: ""
|
||||
val args: List<String> = commandLine.subList(min(1, commandLine.size), commandLine.size)
|
||||
if (exitCode != 0) {
|
||||
val message = if (result.stdout.isBlank() && result.stderr.isBlank()) PySdkBundle.message(
|
||||
"python.conda.permission.denied")
|
||||
else PySdkBundle.message("python.sdk.packaging.non.zero.exit.code", exitCode)
|
||||
PyExecutionException(message, commandLine[0], args, result).let {
|
||||
return@withContext PyResult.failure(it.pyError)
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isTimeout) {
|
||||
PyExecutionException.createForTimeout(PySdkBundle.message("python.sdk.packaging.timed.out"), helperPath, args).let {
|
||||
return@withContext PyResult.failure(it.pyError)
|
||||
}
|
||||
}
|
||||
|
||||
return@withContext PyResult.success(result.stdout)
|
||||
}
|
||||
|
||||
|
||||
@@ -235,4 +152,8 @@ class PipPackageManagerEngine(
|
||||
|
||||
return PyResult.success(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PACKAGING_TOOL_NAME: HelperName = "packaging_tool.py"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user