mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-10-06 06:21:16 +07:00
PY-88280 Add global SDK configuration mutex and wire into SDK setup flows
- Add PythonSdkConfigurationMutex with StateFlow-based lock observation and tryWithLock that throws SdkConfigurationAlreadyInProgressException - Add suspended Module.findPythonSdk() extension that waits for project model to load before resolving - Wrap setupSdk in the global mutex with progress text reporting - Wire mutex into InterpreterFixExecutor, PythonLanguageRuntimeUI - Simplify PyAsyncFileInspectionRunner to use the shared mutex state (cherry picked from commit ace82cf69f1ce88b7a851d063af553f5b6f665d4) GitOrigin-RevId: 02f58779d332886406393410c582d94458c1db65
This commit is contained in:
committed by
intellij-monorepo-bot
parent
1dc7eb5123
commit
4bf4e78ab1
@@ -9,6 +9,7 @@ import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.roots.ModuleRootModificationUtil
|
||||
import com.jetbrains.python.module.PyModuleService
|
||||
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
|
||||
/**
|
||||
* Returns the Python SDK configured for this module, or `null` if none is set.
|
||||
@@ -28,7 +29,9 @@ suspend fun Module.findPythonSdk(): Sdk? {
|
||||
* Prefer the suspended [findPythonSdk] extension in coroutine contexts.
|
||||
*/
|
||||
var Module.pythonSdk: Sdk?
|
||||
@ApiStatus.Obsolete
|
||||
get() = PythonSdkUtil.findPythonSdk(this)
|
||||
@ApiStatus.Internal
|
||||
set(newSdk) {
|
||||
val prevSdk = pythonSdk
|
||||
thisLogger.info("Setting PythonSDK $newSdk to module $this")
|
||||
@@ -39,5 +42,4 @@ var Module.pythonSdk: Sdk?
|
||||
ApplicationManager.getApplication().messageBus.syncPublisher(PySdkListener.TOPIC).moduleSdkUpdated(this, prevSdk, newSdk)
|
||||
}
|
||||
|
||||
|
||||
private val thisLogger = fileLogger()
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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.jetbrains.python.Result
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
|
||||
/**
|
||||
* A [Mutex] wrapper that exposes lock state as an observable [StateFlow].
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
class ObservableMutex {
|
||||
private val delegate = Mutex()
|
||||
private val _isLocked = MutableStateFlow(false)
|
||||
|
||||
/** Observable lock state. `true` while the mutex is held. */
|
||||
val isLocked: StateFlow<Boolean> = _isLocked.asStateFlow()
|
||||
|
||||
/** Acquires the lock, runs [action], and releases the lock. Suspends if already held. */
|
||||
suspend fun <T> withLock(action: suspend () -> T): T {
|
||||
delegate.lock()
|
||||
try {
|
||||
_isLocked.value = true
|
||||
return action()
|
||||
}
|
||||
finally {
|
||||
_isLocked.value = false
|
||||
delegate.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs [action] under the lock if it's not already held.
|
||||
* Returns [Result.Failure] with [AlreadyLocked] if the lock is busy.
|
||||
*/
|
||||
suspend fun <T> tryWithLock(action: suspend () -> T): Result<T, AlreadyLocked> {
|
||||
if (!delegate.tryLock()) return Result.failure(AlreadyLocked)
|
||||
try {
|
||||
_isLocked.value = true
|
||||
return Result.success(action())
|
||||
}
|
||||
finally {
|
||||
_isLocked.value = false
|
||||
delegate.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
/** Sentinel error returned by [tryWithLock] when the lock is already held. */
|
||||
data object AlreadyLocked
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// 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 org.jetbrains.annotations.ApiStatus
|
||||
|
||||
/**
|
||||
* Global mutex that serializes all Python SDK configuration operations — both
|
||||
* auto-configuration on startup and manual SDK creation from the UI.
|
||||
*
|
||||
* This is intentionally global (not per-module) because setting an SDK on one module
|
||||
* can affect others via inherited project SDK in multi-module workspaces.
|
||||
*
|
||||
* Observe [isLocked][ObservableMutex.isLocked] to track whether an SDK configuration is running.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
val pythonSdkConfigurationMutex: ObservableMutex = ObservableMutex()
|
||||
@@ -8,23 +8,29 @@ import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.components.serviceIfCreated
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.jetbrains.python.PythonPluginDisposable
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.util.NlsContexts
|
||||
import com.intellij.platform.ide.progress.withBackgroundProgress
|
||||
import com.intellij.platform.util.coroutines.sync.OverflowSemaphore
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.ui.EditorNotifications
|
||||
import com.intellij.ui.components.ActionLink
|
||||
import com.jetbrains.python.inspections.interpreter.InterpreterFix
|
||||
import com.jetbrains.python.inspections.interpreter.BusyGuardExecutor
|
||||
import com.jetbrains.python.inspections.interpreter.InterpreterFix
|
||||
import com.jetbrains.python.orLogException
|
||||
import com.jetbrains.python.sdk.PySdkListener
|
||||
import com.jetbrains.python.sdk.pythonSdkConfigurationMutex
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import com.intellij.platform.util.coroutines.sync.OverflowSemaphore
|
||||
import kotlinx.coroutines.future.asCompletableFuture
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
@@ -44,11 +50,30 @@ class PyAsyncFileInspectionRunner(
|
||||
cacheTtl: Duration = 20.seconds,
|
||||
private val cacheLoader: suspend (Module) -> InspectionRunnerResult,
|
||||
) {
|
||||
init {
|
||||
// Invalidate cached inspection results when a module's SDK changes, so the notification
|
||||
// panel re-evaluates immediately instead of waiting for the cache TTL to expire.
|
||||
// PythonPluginDisposable is used as parent so the connection is cleaned up on plugin unload.
|
||||
ApplicationManager.getApplication().messageBus.connect(PythonPluginDisposable.getInstance()).subscribe(PySdkListener.TOPIC, object : PySdkListener {
|
||||
override fun moduleSdkUpdated(module: Module, prevSdk: Sdk?, newSdk: Sdk?) {
|
||||
cache.invalidate(module)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private val cache: LoadingCache<Module, Deferred<InspectionRunnerResult>> = Caffeine.newBuilder()
|
||||
.refreshAfterWrite(cacheTtl.toJavaDuration())
|
||||
.weakKeys()
|
||||
.evictionListener<Module, Deferred<InspectionRunnerResult>> { _, value, _ -> value?.cancel() }
|
||||
.build(object : CacheLoader<Module, Deferred<InspectionRunnerResult>> {
|
||||
override fun load(key: Module): Deferred<InspectionRunnerResult> = startComputation(key)
|
||||
override fun load(key: Module): Deferred<InspectionRunnerResult> {
|
||||
// Eagerly remove the cache entry when the module is disposed to break strong reference
|
||||
// chains from cache values (e.g., CreateSdkInfo.sdkCreator capturing Module) that would
|
||||
// otherwise prevent GC of disposed projects.
|
||||
@Suppress("IncorrectParentDisposable")
|
||||
Disposer.register(key) { cache.invalidate(key) }
|
||||
return startComputation(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* On refresh, the old (completed) [Deferred] is served to callers while the new one is loading.
|
||||
@@ -135,9 +160,7 @@ private class CacheEvictingFix(
|
||||
@ApiStatus.Internal
|
||||
@Service(Service.Level.PROJECT)
|
||||
class InterpreterFixExecutor(private val project: Project, internal val scope: CoroutineScope) : BusyGuardExecutor {
|
||||
private val semaphore = OverflowSemaphore(permits = 1, overflow = BufferOverflow.DROP_LATEST)
|
||||
private val _isBusy = MutableStateFlow(false)
|
||||
override val isBusy: StateFlow<Boolean> = _isBusy
|
||||
override val isBusy: StateFlow<Boolean> = pythonSdkConfigurationMutex.isLocked
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
@@ -147,19 +170,7 @@ class InterpreterFixExecutor(private val project: Project, internal val scope: C
|
||||
|
||||
override fun execute(action: suspend () -> Unit) {
|
||||
scope.launch {
|
||||
semaphore.withPermit {
|
||||
_isBusy.value = true
|
||||
try {
|
||||
action()
|
||||
}
|
||||
finally {
|
||||
_isBusy.value = false
|
||||
}
|
||||
}
|
||||
}.invokeOnCompletion { cause ->
|
||||
if (cause != null && _isBusy.value) {
|
||||
LOG.warn("Interpreter fix submission discarded: another fix is already in progress")
|
||||
}
|
||||
pythonSdkConfigurationMutex.tryWithLock { action() }.orLogException(LOG)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ import com.jetbrains.python.errorProcessing.emit
|
||||
import com.jetbrains.python.sdk.ModuleOrProject
|
||||
import com.jetbrains.python.sdk.add.collector.PythonNewInterpreterAddedCollector
|
||||
import com.jetbrains.python.sdk.configuration.CreateSdkInfoWithTool
|
||||
import com.jetbrains.python.sdk.moduleIfExists
|
||||
import com.jetbrains.python.sdk.pythonSdk
|
||||
import com.jetbrains.python.sdk.service.PySdkService.Companion.pySdkService
|
||||
import com.jetbrains.python.venvReader.VirtualEnvReader
|
||||
import kotlinx.coroutines.Deferred
|
||||
@@ -51,7 +53,6 @@ class PythonAddLocalInterpreterPresenter(
|
||||
return
|
||||
}
|
||||
is Result.Success -> {
|
||||
moduleOrProject.project.pySdkService.persistSdk(r.result)
|
||||
val isPreviouslyConfigured = addEnvironment.createStatisticsInfo(PythonInterpreterCreationTargets.LOCAL_MACHINE).previouslyConfigured
|
||||
PythonNewInterpreterAddedCollector.logPythonNewInterpreterAdded(r.result, isPreviouslyConfigured)
|
||||
_sdkShared.emit(r.result)
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.intellij.notification.NotificationAction
|
||||
import com.intellij.notification.NotificationGroupManager
|
||||
import com.intellij.notification.NotificationType
|
||||
import com.intellij.notification.NotificationsManager
|
||||
import com.intellij.openapi.application.writeAction
|
||||
import com.intellij.openapi.diagnostic.getOrLogException
|
||||
import com.intellij.openapi.help.HelpManager
|
||||
import com.intellij.openapi.module.Module
|
||||
@@ -14,7 +13,6 @@ import com.intellij.openapi.observable.properties.AtomicProperty
|
||||
import com.intellij.openapi.observable.properties.ObservableProperty
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED
|
||||
@@ -26,6 +24,7 @@ import com.intellij.platform.ide.progress.ModalTaskOwner
|
||||
import com.intellij.platform.ide.progress.TaskCancellation
|
||||
import com.intellij.platform.ide.progress.withBackgroundProgress
|
||||
import com.intellij.platform.ide.progress.withModalProgress
|
||||
import com.intellij.platform.util.progress.withProgressText
|
||||
import com.intellij.python.common.tools.ToolId
|
||||
import com.intellij.python.community.execService.Args
|
||||
import com.intellij.python.community.execService.BinaryToExec
|
||||
@@ -52,10 +51,10 @@ import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
|
||||
import com.jetbrains.python.sdk.LOGGER
|
||||
import com.jetbrains.python.sdk.ModuleOrProject
|
||||
import com.jetbrains.python.sdk.PythonSdkType
|
||||
import com.jetbrains.python.sdk.configuration.CONDA_TOOL_ID
|
||||
import com.jetbrains.python.sdk.configuration.PIPENV_TOOL_ID
|
||||
import com.jetbrains.python.sdk.configuration.VENV_TOOL_ID
|
||||
import com.jetbrains.python.sdk.createSdk
|
||||
import com.jetbrains.python.sdk.excludeInnerVirtualEnv
|
||||
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
|
||||
import com.jetbrains.python.sdk.flavors.PyFlavorData
|
||||
@@ -63,6 +62,9 @@ import com.jetbrains.python.sdk.flavors.VirtualEnvSdkFlavor
|
||||
import com.jetbrains.python.sdk.installSdkIfNeeded
|
||||
import com.jetbrains.python.sdk.moduleIfExists
|
||||
import com.jetbrains.python.sdk.persist
|
||||
import com.jetbrains.python.sdk.pythonSdk
|
||||
import com.jetbrains.python.sdk.pythonSdkConfigurationMutex
|
||||
import com.jetbrains.python.sdk.service.PySdkService.Companion.pySdkService
|
||||
import com.jetbrains.python.sdk.setAssociationToModule
|
||||
import com.jetbrains.python.sdk.suggestAssociatedSdkName
|
||||
import com.jetbrains.python.statistics.InterpreterTarget
|
||||
@@ -97,14 +99,18 @@ abstract class PythonAddEnvironment<P : PathHolder>(open val model: PythonAddInt
|
||||
*/
|
||||
protected abstract suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk>
|
||||
|
||||
protected suspend fun setupSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
|
||||
protected suspend fun setupSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> = pythonSdkConfigurationMutex.withLock {
|
||||
savePathToExecutableToProperties(null)
|
||||
val sdk = getOrCreateSdk(moduleOrProject).getOr { return it }
|
||||
val sdk = getOrCreateSdk(moduleOrProject).getOr { return@withLock it }
|
||||
|
||||
moduleOrProject.project.pySdkService.persistSdk(sdk)
|
||||
moduleOrProject.project.excludeInnerVirtualEnv(sdk)
|
||||
moduleOrProject.moduleIfExists?.let { sdk.setAssociationToModule(it) }
|
||||
moduleOrProject.moduleIfExists?.let {
|
||||
it.pythonSdk = sdk
|
||||
sdk.setAssociationToModule(it)
|
||||
}
|
||||
|
||||
return Result.success(sdk)
|
||||
Result.success(sdk)
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
@@ -233,13 +239,11 @@ internal fun installBaseSdk(sdk: Sdk, existingSdks: List<Sdk>): Sdk? {
|
||||
|
||||
internal suspend fun <P : PathHolder> setupSdk(
|
||||
project: Project?,
|
||||
allSdks: List<Sdk>,
|
||||
fileSystem: FileSystem<P>,
|
||||
pythonBinaryPath: P,
|
||||
languageLevel: LanguageLevel,
|
||||
targetPanelExtension: TargetPanelExtension?,
|
||||
): PyResult<Sdk> {
|
||||
val sdkType = PythonSdkType.getInstance()
|
||||
|
||||
val (additionalData, customSdkSuggestedName) = when (fileSystem) {
|
||||
is FileSystem.Eel -> null to suggestAssociatedSdkName(pythonBinaryPath.toString(), project?.basePath)
|
||||
@@ -257,28 +261,15 @@ internal suspend fun <P : PathHolder> setupSdk(
|
||||
}
|
||||
}
|
||||
|
||||
val sdk = SdkConfigurationUtil.createSdk(
|
||||
allSdks,
|
||||
pythonBinaryPath.toString(),
|
||||
sdkType,
|
||||
additionalData,
|
||||
customSdkSuggestedName
|
||||
return createSdk(
|
||||
pythonBinaryPath,
|
||||
customSdkSuggestedName,
|
||||
additionalData
|
||||
)
|
||||
|
||||
sdk.sdkModificator.let { modifiableSdk ->
|
||||
modifiableSdk.versionString = languageLevel.toPythonVersion()
|
||||
writeAction {
|
||||
modifiableSdk.commitChanges()
|
||||
}
|
||||
}
|
||||
|
||||
sdkType.setupSdkPaths(sdk)
|
||||
return PyResult.success(sdk)
|
||||
}
|
||||
|
||||
internal suspend fun <P : PathHolder> PythonSelectableInterpreter<P>.setupSdk(
|
||||
moduleOrProject: ModuleOrProject,
|
||||
allSdks: List<Sdk>,
|
||||
fileSystem: FileSystem<P>,
|
||||
targetPanelExtension: TargetPanelExtension?,
|
||||
isAssociateWithModule: Boolean,
|
||||
@@ -289,7 +280,6 @@ internal suspend fun <P : PathHolder> PythonSelectableInterpreter<P>.setupSdk(
|
||||
|
||||
val newSdk = setupSdk(
|
||||
project = moduleOrProject.project,
|
||||
allSdks = allSdks,
|
||||
fileSystem = fileSystem,
|
||||
pythonBinaryPath = homePath!!,
|
||||
languageLevel = pythonInfo.languageLevel,
|
||||
|
||||
@@ -51,7 +51,6 @@ class PythonExistingEnvironmentSelector<P : PathHolder>(model: PythonAddInterpre
|
||||
// todo error handling, nullability issues
|
||||
val sdk = model.state.selectedInterpreter.get()!!.setupSdk(
|
||||
moduleOrProject = moduleOrProject,
|
||||
allSdks = model.existingSdks,
|
||||
fileSystem = model.fileSystem,
|
||||
targetPanelExtension = model.state.targetPanelExtension.get(),
|
||||
isAssociateWithModule = true,
|
||||
|
||||
@@ -34,7 +34,6 @@ suspend fun <P : PathHolder> PythonMutableTargetAddInterpreterModel<P>.setupVirt
|
||||
moduleOrProject = moduleOrProject,
|
||||
pathToBasePython = baseSdkPath,
|
||||
pathToVenvHome = venvFolder,
|
||||
existingSdks = existingSdks
|
||||
).getOr { return it }
|
||||
|
||||
return PyResult.success(newSdk.sdk)
|
||||
@@ -44,7 +43,6 @@ private suspend fun <P : PathHolder> PythonAddInterpreterModel<P>.createSdkFromB
|
||||
moduleOrProject: ModuleOrProject,
|
||||
pathToBasePython: P,
|
||||
pathToVenvHome: P,
|
||||
existingSdks: List<Sdk>,
|
||||
): PyResult<SdkWrapper<P>> {
|
||||
val basePython = fileSystem.getBinaryToExec(pathToBasePython)
|
||||
val inheritSitePackages = venvViewModel.inheritSitePackages.get()
|
||||
@@ -57,7 +55,6 @@ private suspend fun <P : PathHolder> PythonAddInterpreterModel<P>.createSdkFromB
|
||||
|
||||
val sdkResult = detectedSelectableInterpreter.setupSdk(
|
||||
moduleOrProject = moduleOrProject,
|
||||
allSdks = existingSdks,
|
||||
fileSystem = fileSystem,
|
||||
targetPanelExtension = state.targetPanelExtension.get(),
|
||||
isAssociateWithModule = !venvViewModel.makeAvailableForAllProjects.get()
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.jetbrains.python.sdk.add.v2.PythonAddCustomInterpreter
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode
|
||||
import com.jetbrains.python.sdk.add.v2.PythonLocalAddInterpreterModel
|
||||
import com.jetbrains.python.sdk.configurePythonSdk
|
||||
import com.jetbrains.python.sdk.pythonSdkConfigurationMutex
|
||||
import com.jetbrains.python.sdk.service.PySdkService.Companion.pySdkService
|
||||
import com.jetbrains.python.util.ShowingMessageErrorSync
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
@@ -116,12 +117,14 @@ class PythonLanguageRuntimeUI(
|
||||
|
||||
val sdk = runWithModalProgressBlocking(project, message("python.sdk.progress.setting.up.environment")) {
|
||||
withContext(TraceContext(message("trace.context.add.remote.python.sdk.dialog", targetSupplier.get().getTargetType().displayName))) {
|
||||
sdkManager.getOrCreateSdkWithModal(ModuleOrProject.ModuleAndProject(module)).onFailure {
|
||||
errorSink.emit(it)
|
||||
}.successOrNull?.also {
|
||||
configurePythonSdk(project, module, it)
|
||||
project.pySdkService.persistSdk(it)
|
||||
PythonNewInterpreterAddedCollector.logPythonNewInterpreterAdded(it, false)
|
||||
pythonSdkConfigurationMutex.withLock {
|
||||
sdkManager.getOrCreateSdkWithModal(ModuleOrProject.ModuleAndProject(module)).onFailure {
|
||||
errorSink.emit(it)
|
||||
}.successOrNull?.also {
|
||||
configurePythonSdk(project, module, it)
|
||||
project.pySdkService.persistSdk(it)
|
||||
PythonNewInterpreterAddedCollector.logPythonNewInterpreterAdded(it, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user