User: kyle wood Date: 19 Jul 26 21:30 Revision: 72d0a8e593c983b7ede0f74f1f56445eb072b0f4 Summary: Fix Write-unsafe context errors on project creation Fixes #2622 Fixes #2592 Fixes #2472 Fixes #2419 TeamCity URL: http://ci.mcdev.io:80/viewModification.html?tab=vcsModificationFiles&modId=10575&personal=false Index: build.gradle.kts =================================================================== --- build.gradle.kts (revision 3b11613afdd1bcabcb60e039eb6bd154a4d59341) +++ build.gradle.kts (revision 72d0a8e593c983b7ede0f74f1f56445eb072b0f4) @@ -121,6 +121,7 @@ bundledModule("intellij.platform.langInjection") bundledPlugin("com.intellij.properties") bundledPlugin("Git4Idea") + bundledModule("intellij.platform.collaborationTools") bundledPlugin("com.intellij.modules.json") // Optional dependencies Index: src/main/kotlin/creator/JdkComboBoxWithPreference.kt =================================================================== --- src/main/kotlin/creator/JdkComboBoxWithPreference.kt (revision 3b11613afdd1bcabcb60e039eb6bd154a4d59341) +++ src/main/kotlin/creator/JdkComboBoxWithPreference.kt (revision 72d0a8e593c983b7ede0f74f1f56445eb072b0f4) @@ -41,6 +41,7 @@ import com.intellij.openapi.util.Disposer import com.intellij.ui.dsl.builder.Cell import com.intellij.ui.dsl.builder.Row +import com.intellij.util.SlowOperations import javax.swing.JComponent internal class JdkPreferenceData( @@ -176,7 +177,13 @@ } val lastUsedSdk = stateComponent.getValue(selectedJdkProperty) + // preselectJdkForNewModule -> setSelectedJdk -> combo renderer -> LocalFileSystem.findFileByPath + // triggers a SlowOperations violation since the combo renderer does VFS I/O on the EDT. + // This is entirely platform code we don't own, so allowSlowOperations is the only "solution". + @Suppress("UnstableApiUsage") + SlowOperations.knownIssue("IDEA-364789").use { - ProjectWizardUtil.preselectJdkForNewModule(project, lastUsedSdk, comboBox) { true } + ProjectWizardUtil.preselectJdkForNewModule(project, lastUsedSdk, comboBox) { true } + } val windowChild = context.getUserData(AbstractWizard.KEY)!!.contentPanel comboBox.loadSuggestions(windowChild, context.disposable) Index: src/main/kotlin/creator/custom/CreatorTemplateProcessor.kt =================================================================== --- src/main/kotlin/creator/custom/CreatorTemplateProcessor.kt (revision 3b11613afdd1bcabcb60e039eb6bd154a4d59341) +++ src/main/kotlin/creator/custom/CreatorTemplateProcessor.kt (revision 72d0a8e593c983b7ede0f74f1f56445eb072b0f4) @@ -230,7 +230,7 @@ val templateProperties = collectTemplateProperties() thisLogger().debug("Template properties: $templateProperties") - val generatedFiles = mutableListOf>() + val generatedFiles = mutableListOf>() for (file in template.descriptor.files.orEmpty()) { if (file.condition != null && !TemplateEvaluator.condition(templateProperties, file.condition).getOrElse { false } @@ -270,12 +270,7 @@ destPath.parent.createDirectories() destPath.writeText(processedContent) - val virtualFile = destPath.refreshAndFindVirtualFile() - if (virtualFile != null) { - generatedFiles.add(file to virtualFile) - } else { - thisLogger().warn("Could not find VirtualFile for file generated at $destPath (descriptor: $file)") - } + generatedFiles.add(file to destPath) } catch (t: Throwable) { if (t is ControlFlowException) { throw t @@ -291,9 +286,13 @@ // Apparently a module root is required for the reformat to work setupTempRootModule(project, projectPath) - reformatFiles(project, generatedFiles) - openFilesInEditor(project, generatedFiles) + val genVirtualFiles = generatedFiles + .mapNotNullTo(mutableListOf()) { + it.first to (it.second.refreshAndFindVirtualFile() ?: return@mapNotNullTo null) - } + } + reformatFiles(project, genVirtualFiles) + openFilesInEditor(project, genVirtualFiles) + } val finalizers = template.descriptor.finalizers if (!finalizers.isNullOrEmpty()) { Index: src/main/kotlin/creator/custom/CustomPlatformStep.kt =================================================================== --- src/main/kotlin/creator/custom/CustomPlatformStep.kt (revision 3b11613afdd1bcabcb60e039eb6bd154a4d59341) +++ src/main/kotlin/creator/custom/CustomPlatformStep.kt (revision 72d0a8e593c983b7ede0f74f1f56445eb072b0f4) @@ -56,6 +56,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * The step to select a custom template repo. @@ -219,7 +220,7 @@ val indicator = CreatorProgressIndicator( templateProvidersLoadingProperty, templateProvidersTextProperty, - templateProvidersText2Property + templateProvidersText2Property, ) templateProvidersTextProperty.set(MCDevBundle("creator.step.generic.init_template_providers.message")) @@ -233,20 +234,26 @@ val dialogCoroutineContext = context.modalityState.asContextElement() val uiContext = dialogCoroutineContext + Dispatchers.EDT - creatorUiScope.launch(uiContext) { + creatorUiScope.launch(dialogCoroutineContext) { + // provider.init() downloads/reads files, so run in the background + withContext(Dispatchers.Default) { - for ((providerKey, repos) in templateRepos.groupBy { it.provider }) { - val provider = TemplateProvider.get(providerKey) - ?: continue + for ((providerKey, repos) in templateRepos.groupBy { it.provider }) { + val provider = TemplateProvider.get(providerKey) + ?: continue - indicator.text = provider.label + // indicator.text is a PropertyGraph property, so set it on EDT. + withContext(uiContext) { indicator.text = provider.label } - runCatching { provider.init(indicator, repos) } - .getOrLogException(logger()) - } + runCatching { provider.init(indicator, repos) } + .getOrLogException(logger()) + } + } + withContext(uiContext) { - templateProvidersLoadingProperty.set(false) - // Force refresh to trigger template loading - templateRepoProperty.set(templateRepo) - } - } + templateProvidersLoadingProperty.set(false) + // Force refresh to trigger template loading + templateRepoProperty.set(templateRepo) + } + } + } private fun loadTemplatesInBackground(provider: suspend () -> Collection) { selectedTemplate = EmptyLoadedTemplate @@ -263,22 +270,28 @@ val dialogCoroutineContext = context.modalityState.asContextElement() val uiContext = dialogCoroutineContext + Dispatchers.EDT templateLoadingJob?.cancel("Another template has been selected") - templateLoadingJob = creatorUiScope.launch(uiContext) { - val newTemplates = runCatching { provider() } + templateLoadingJob = creatorUiScope.launch(dialogCoroutineContext) { + // Run slow VFS/IO work on a background thread, not the EDT. + val newTemplates = withContext(Dispatchers.Default) { + runCatching { provider() } - .getOrLogException(logger()) - ?: emptyList() + .getOrLogException(logger()) + ?: emptyList() + } + // Switch to EDT only for UI property mutations. + withContext(uiContext) { - templateLoadingProperty.set(false) - noTemplatesAvailable.visible(newTemplates.isEmpty()) - if (newTemplates.any { checkInvalidVersionTemplate(it) }) { - availableTemplates = emptyList() - invalidTemplateVersion.visible(true) - } else { - availableTemplates = newTemplates - invalidTemplateVersion.visible(false) - } - } - } + templateLoadingProperty.set(false) + noTemplatesAvailable.visible(newTemplates.isEmpty()) + if (newTemplates.any { checkInvalidVersionTemplate(it) }) { + availableTemplates = emptyList() + invalidTemplateVersion.visible(true) + } else { + availableTemplates = newTemplates + invalidTemplateVersion.visible(false) + } + } + } + } private fun checkInvalidVersionTemplate(template: LoadedTemplate): Boolean { if (template is InvalidVersionTemplate) { Index: src/main/kotlin/creator/custom/providers/TemplateProvider.kt =================================================================== --- src/main/kotlin/creator/custom/providers/TemplateProvider.kt (revision 3b11613afdd1bcabcb60e039eb6bd154a4d59341) +++ src/main/kotlin/creator/custom/providers/TemplateProvider.kt (revision 72d0a8e593c983b7ede0f74f1f56445eb072b0f4) @@ -34,6 +34,7 @@ import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.extensions.RequiredElement +import com.intellij.openapi.progress.ProcessCanceledException import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.util.KeyedExtensionCollector import com.intellij.openapi.vfs.VfsUtilCore @@ -45,6 +46,7 @@ import com.intellij.util.KeyedLazyInstance import com.intellij.util.xmlb.annotations.Attribute import java.util.ResourceBundle +import java.util.concurrent.CancellationException import javax.swing.JComponent /** @@ -99,7 +101,7 @@ createVfsLoadedTemplate(modalityState, file.parent, file, bundle = bundle) ?.let(templates::add) } catch (t: Throwable) { - if (t is ControlFlowException) { + if (t is ProcessCanceledException || t is ControlFlowException || t is CancellationException) { throw t } @@ -134,7 +136,7 @@ languageBundle ) } catch (t: Throwable) { - if (t is ControlFlowException) { + if (t is ProcessCanceledException || t is ControlFlowException || t is CancellationException) { throw t } @@ -152,9 +154,14 @@ } try { - return file.refreshSync(modalityState) - ?.inputStream?.reader()?.use { TemplateResourceBundle(it, parent) } + val refreshedFile = file.refreshSync(modalityState) + if (refreshedFile != null) { + return refreshedFile.inputStream.reader().use { TemplateResourceBundle(it, parent) } + } } catch (t: Throwable) { + if (t is ProcessCanceledException || t is CancellationException) { + throw t + } if (t is ControlFlowException) { return parent } @@ -194,7 +201,7 @@ if (descriptor.inherit != null) { val parent = templateRoot.findFileByRelativePath(descriptor.inherit) if (parent != null) { - parent.refresh(false, false) + parent.refreshSync(modalityState) val parentDescriptor = Gson().fromJson(parent.readText()) val mergedProperties = parentDescriptor.properties.orEmpty() + descriptor.properties.orEmpty() val mergedFiles = parentDescriptor.files.orEmpty() + descriptor.files.orEmpty() Index: src/main/kotlin/creator/custom/providers/VfsLoadedTemplate.kt =================================================================== --- src/main/kotlin/creator/custom/providers/VfsLoadedTemplate.kt (revision 3b11613afdd1bcabcb60e039eb6bd154a4d59341) +++ src/main/kotlin/creator/custom/providers/VfsLoadedTemplate.kt (revision 72d0a8e593c983b7ede0f74f1f56445eb072b0f4) @@ -34,10 +34,8 @@ ) : LoadedTemplate { override fun loadTemplateContents(path: String): String { - templateRoot.refresh(false, true) val virtualFile = templateRoot.findFileByRelativePath(path) ?: throw FileNotFoundException("Could not find file $path in template root ${templateRoot.path}") - virtualFile.refresh(false, false) return virtualFile.readText() } } Index: src/main/kotlin/util/files.kt =================================================================== --- src/main/kotlin/util/files.kt (revision 3b11613afdd1bcabcb60e039eb6bd154a4d59341) +++ src/main/kotlin/util/files.kt (revision 72d0a8e593c983b7ede0f74f1f56445eb072b0f4) @@ -20,9 +20,9 @@ package com.demonwav.mcdev.util -import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.EDT import com.intellij.openapi.application.ModalityState -import com.intellij.openapi.application.writeAction +import com.intellij.openapi.application.TransactionGuard import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VfsUtilCore import com.intellij.openapi.vfs.VirtualFile @@ -33,6 +33,10 @@ import java.util.jar.Attributes import java.util.jar.JarFile import java.util.jar.Manifest +import kotlin.coroutines.resume +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext val VirtualFile.localFile: File get() = VfsUtilCore.virtualToIoFile(this) @@ -80,17 +84,22 @@ operator fun Manifest.get(attribute: Attributes.Name): String? = mainAttributes.getValue(attribute) suspend fun VirtualFile.refreshSync(modalityState: ModalityState): VirtualFile? { - fun refresh() { - RefreshQueue.getInstance().refresh(false, this.isDirectory, null, modalityState, this) + val file = this + return withContext(Dispatchers.EDT) { + val state = if (TransactionGuard.getInstance().isWriteSafeModality(modalityState)) { + modalityState + } else { + ModalityState.current() - } + } - if (ApplicationManager.getApplication().isWriteAccessAllowed) { - refresh() - } else { - writeAction { - refresh() + return@withContext suspendCancellableCoroutine { continuation -> + try { + RefreshQueue.getInstance().refresh(true, file.isDirectory, { + continuation.resume(file.parent?.findChild(file.name)) + }, state, file) + } catch (t: Throwable) { + continuation.cancel(t) - } - } + } + } - - return this.parent?.findOrCreateChildData(this, this.name) -} + } +} Index: src/main/resources/META-INF/plugin.xml =================================================================== --- src/main/resources/META-INF/plugin.xml (revision 3b11613afdd1bcabcb60e039eb6bd154a4d59341) +++ src/main/resources/META-INF/plugin.xml (revision 72d0a8e593c983b7ede0f74f1f56445eb072b0f4) @@ -20,14 +20,18 @@ - com.intellij.modules.java - org.jetbrains.idea.maven - com.intellij.gradle + + + + + + + + + + + org.jetbrains.kotlin - org.intellij.groovy - com.intellij.properties - com.intellij.modules.json - ByteCodeViewer org.toml.lang org.jetbrains.plugins.yaml