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
This commit is contained in:
Vitaly Legchilkin
2026-02-14 04:26:28 +00:00
committed by intellij-monorepo-bot
parent a29f7ae0bf
commit 96a42dbdb0
2 changed files with 20 additions and 4 deletions
@@ -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());
@@ -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()) }