From 96a42dbdb0a7c5f00ac8be2265c82e51ed963171 Mon Sep 17 00:00:00 2001 From: Vitaly Legchilkin Date: Fri, 13 Feb 2026 22:08:53 +0100 Subject: [PATCH] PY-87104 Stop skeleton generation processes when SDK is disposed Register the indicator disposable under the SDK disposable (instead of PythonPluginDisposable) so that when the SDK is disposed the progress indicator is cancelled immediately. This terminates any external Python processes (e.g., skeleton generation) that are still running. In PyTargetsSkeletonGenerator, use the indicator-aware CapturingProcessRunner.runProcess(indicator) which polls indicator.isCanceled() every 10 ms and destroys the process when cancelled, instead of the no-arg runProcess() that waits indefinitely. Together these changes fix thread leaks in tests where SDK disposal during teardown left orphaned BaseDataReader threads from skeleton generation subprocesses. GitOrigin-RevId: a8f748ae0f03645a843ed3c62caa0260b2b9a33d --- .../com/jetbrains/python/sdk/PythonSdkUpdater.java | 14 +++++++++++--- .../sdk/skeletons/PyTargetsSkeletonGenerator.kt | 10 +++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java b/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java index a84f70c07a1c..9120ea6f50cc 100644 --- a/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java +++ b/python/src/com/jetbrains/python/sdk/PythonSdkUpdater.java @@ -264,9 +264,15 @@ public final class PythonSdkUpdater { if (isSdkDisposed()) { return; } - // This explicit cancellation should become unnecessary on migrating PythonSdkUpdater to coroutines and withBackgroundProgress + // Cancel the indicator when the SDK is disposed to terminate any running processes (e.g., skeleton generation). + // This explicit cancellation should become unnecessary on migrating PythonSdkUpdater to coroutines and withBackgroundProgress. Disposable indicatorDisposable = getIndicatorDisposable(indicator); - Disposer.register(PythonPluginDisposable.getInstance(myProject), indicatorDisposable); + if (mySdk instanceof Disposable sdkDisposable) { + Disposer.register(sdkDisposable, indicatorDisposable); + } + else { + Disposer.register(PythonPluginDisposable.getInstance(myProject), indicatorDisposable); + } if (Trigger.LOG.isDebugEnabled()) { Trigger.LOG.debug( "Starting SDK refresh for '" + mySdk.getName() + "' triggered by " + Trigger.getCauseByTrace(myRequestData.myTraceback)); @@ -294,7 +300,9 @@ public final class PythonSdkUpdater { } finally { ApplicationManager.getApplication().invokeLater(() -> { - Disposer.dispose(indicatorDisposable); + if (!isSdkDisposed()) { + Disposer.dispose(indicatorDisposable); + } // restart code analysis DaemonCodeAnalyzer.getInstance(myProject).restart(this); }, myProject.getDisposed()); diff --git a/python/src/com/jetbrains/python/sdk/skeletons/PyTargetsSkeletonGenerator.kt b/python/src/com/jetbrains/python/sdk/skeletons/PyTargetsSkeletonGenerator.kt index d41882375242..678ee3f27851 100644 --- a/python/src/com/jetbrains/python/sdk/skeletons/PyTargetsSkeletonGenerator.kt +++ b/python/src/com/jetbrains/python/sdk/skeletons/PyTargetsSkeletonGenerator.kt @@ -2,6 +2,7 @@ package com.jetbrains.python.sdk.skeletons import com.intellij.execution.process.CapturingProcessHandler +import com.intellij.execution.process.CapturingProcessRunner import com.intellij.execution.process.ProcessOutput import com.intellij.execution.target.TargetEnvironment import com.intellij.execution.target.TargetEnvironmentRequest @@ -13,6 +14,7 @@ import com.intellij.execution.target.value.getTargetDownloadPath import com.intellij.execution.target.value.getTargetUploadPath import com.intellij.openapi.progress.EmptyProgressIndicator import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.projectRoots.Sdk @@ -128,7 +130,13 @@ class PyTargetsSkeletonGenerator(skeletonPath: String, pySdk: Sdk, currentFolder val commandPresentation = targetedCommandLine.getCommandPresentation(targetEnvironment) val capturingProcessHandler = CapturingProcessHandler(process, targetedCommandLine.charset, commandPresentation) listener?.let { capturingProcessHandler.addProcessListener(LineWiseProcessOutputListener.Adapter(it)) } - val result = capturingProcessHandler.runProcess() + val indicator = ProgressManager.getInstance().progressIndicator + val result = if (indicator != null) { + CapturingProcessRunner(capturingProcessHandler).runProcess(indicator) + } + else { + capturingProcessHandler.runProcess() + } // XXX Make it automatic targetEnvironment.downloadVolumes.values.forEach { it.download(".", EmptyProgressIndicator()) }