From 8fb19c87ead3175130a658e76372a659adb61a48 Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Tue, 5 Mar 2024 09:10:20 +0100 Subject: [PATCH] fix(mps-legacy-sync-plugin): use legacy plugin parts This ensures that persisted binding configuration is loaded when a project is opened. Also, minor bugs where fixed that were discovered in tests during initialization and dispose of project plugin parts. --- .../model/mpsplugin/ModelServerConnection.kt | 5 +- .../mpsplugin/history/CloudRootTreeNode.kt | 17 +++- .../projectview/CloudProjectViewExtension.kt | 23 +++-- .../mps/sync/ModelSyncProjectService.kt | 36 ++++++++ .../org/modelix/mps/sync/ModelSyncService.kt | 16 +++- .../kotlin/PersistedBindingsLoadingTest.kt | 85 +++++++++++++++++++ .../src/test/kotlin/SyncPluginTestBase.kt | 25 ++++-- .../.mps/.gitignore | 3 + .../.mps/cloudResources.xml | 15 ++++ .../.mps/migration.xml | 6 ++ .../.mps/modules.xml | 8 ++ .../projectWithPersistedBindings/.mps/vcs.xml | 6 ++ .../solutions/NewSolution/NewSolution.msd | 22 +++++ .../models/NewSolution.a_model.mps | 31 +++++++ 14 files changed, 277 insertions(+), 21 deletions(-) create mode 100644 mps-legacy-sync-plugin/src/main/kotlin/org/modelix/mps/sync/ModelSyncProjectService.kt create mode 100644 mps-legacy-sync-plugin/src/test/kotlin/PersistedBindingsLoadingTest.kt create mode 100644 mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/.mps/.gitignore create mode 100644 mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/.mps/cloudResources.xml create mode 100644 mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/.mps/migration.xml create mode 100644 mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/.mps/modules.xml create mode 100644 mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/.mps/vcs.xml create mode 100644 mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/solutions/NewSolution/NewSolution.msd create mode 100644 mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/solutions/NewSolution/models/NewSolution.a_model.mps diff --git a/mps-legacy-sync-plugin/src/main/java/org/modelix/model/mpsplugin/ModelServerConnection.kt b/mps-legacy-sync-plugin/src/main/java/org/modelix/model/mpsplugin/ModelServerConnection.kt index 478e2a6b..ee038955 100644 --- a/mps-legacy-sync-plugin/src/main/java/org/modelix/model/mpsplugin/ModelServerConnection.kt +++ b/mps-legacy-sync-plugin/src/main/java/org/modelix/model/mpsplugin/ModelServerConnection.kt @@ -47,6 +47,7 @@ import org.modelix.model.mpsadapters.mps.SConceptAdapter import org.modelix.model.mpsadapters.mps.SNodeAPI import org.modelix.model.mpsplugin.plugin.EModelixExecutionMode import org.modelix.model.mpsplugin.plugin.ModelixConfigurationSystemProperties +import org.modelix.mps.sync.ModelSyncService import java.util.Arrays import java.util.Objects import java.util.function.Consumer @@ -95,11 +96,13 @@ class ModelServerConnection @JvmOverloads constructor(baseUrl: String, providedH val workspaceTokenProvider: () -> String? = { InstanceJwtToken.token } val tokenProvider: (() -> String?)? = (if (ModelixConfigurationSystemProperties.executionMode == EModelixExecutionMode.PROJECTOR) workspaceTokenProvider else null) + val syncService = ApplicationManager.getApplication().getService(ModelSyncService::class.java) + val httpClient = providedHttpClient ?: syncService.httpClient client = RestWebModelClient( baseUrl, tokenProvider, Arrays.asList(ConnectionListenerForForbiddenMessage(baseUrl)), - providedHttpClient, + httpClient, ) val connectedFirstTime: Wrappers._boolean = Wrappers._boolean(true) client.addStatusListener({ oldStatus: RestWebModelClient.ConnectionStatus?, newStatus: RestWebModelClient.ConnectionStatus -> diff --git a/mps-legacy-sync-plugin/src/main/java/org/modelix/model/mpsplugin/history/CloudRootTreeNode.kt b/mps-legacy-sync-plugin/src/main/java/org/modelix/model/mpsplugin/history/CloudRootTreeNode.kt index 32b742f3..dd8db893 100644 --- a/mps-legacy-sync-plugin/src/main/java/org/modelix/model/mpsplugin/history/CloudRootTreeNode.kt +++ b/mps-legacy-sync-plugin/src/main/java/org/modelix/model/mpsplugin/history/CloudRootTreeNode.kt @@ -1,16 +1,29 @@ package org.modelix.model.mpsplugin.history +import jetbrains.mps.ide.ThreadUtils import jetbrains.mps.ide.ui.tree.TextTreeNode +import org.apache.log4j.LogManager +import org.apache.log4j.Logger import org.modelix.model.mpsplugin.CloudIcons import org.modelix.model.mpsplugin.ModelServerConnections /*Generated by MPS */ class CloudRootTreeNode : TextTreeNode(CloudIcons.ROOT_ICON, "Cloud") { + + companion object { + private val LOG: Logger = LogManager.getLogger(CloudRootTreeNode::class.java) + } + private var myInitialized: Boolean = false private val repositoriesListener: ModelServerConnections.IListener = object : ModelServerConnections.IListener { override fun repositoriesChanged() { - update() - init() + val exception = ThreadUtils.runInUIThreadAndWait { + update() + init() + } + if (exception != null) { + LOG.error("Failed to update cloud view.", exception) + } } } diff --git a/mps-legacy-sync-plugin/src/main/java/org/modelix/model/mpsplugin/projectview/CloudProjectViewExtension.kt b/mps-legacy-sync-plugin/src/main/java/org/modelix/model/mpsplugin/projectview/CloudProjectViewExtension.kt index 9e842d3d..b2e1ddb4 100644 --- a/mps-legacy-sync-plugin/src/main/java/org/modelix/model/mpsplugin/projectview/CloudProjectViewExtension.kt +++ b/mps-legacy-sync-plugin/src/main/java/org/modelix/model/mpsplugin/projectview/CloudProjectViewExtension.kt @@ -114,17 +114,17 @@ class CloudProjectViewExtension(private val project: Project?) { } } + var waitForProjectTreeTimer: Timer? = null + fun init() { cloudTreeNode = TextTreeNode("Cloud") cloudTreeNode!!.icon = ROOT_ICON - waitForProjectTree(object : _void_P1_E0 { - override fun invoke(tree: ProjectTree?) { - treeModel = TreeModelUtil.getModel(tree) - treeModel!!.addTreeModelListener(treeListener) - project!!.repository.addRepositoryListener(repositoryListener) - updateModules() - } - }) + waitForProjectTree { tree -> + treeModel = TreeModelUtil.getModel(tree) + treeModel!!.addTreeModelListener(treeListener) + project!!.repository.addRepositoryListener(repositoryListener) + updateModules() + } } private fun waitForProjectTree(callback: _void_P1_E0) { @@ -146,6 +146,10 @@ class CloudProjectViewExtension(private val project: Project?) { }, ) timer.value!!.start() + assert(waitForProjectTreeTimer == null) { + "waitForProjectTreeTimer should not have been initialized." + } + waitForProjectTreeTimer = timer.value } } @@ -164,6 +168,7 @@ class CloudProjectViewExtension(private val project: Project?) { } fun dispose() { + waitForProjectTreeTimer?.stop() project!!.repository.removeRepositoryListener(repositoryListener) if (treeModel != null) { treeModel!!.removeTreeModelListener(treeListener) @@ -233,7 +238,7 @@ class CloudProjectViewExtension(private val project: Project?) { } fun updateModules() { - val root: MPSTreeNode? = projectTree!!.rootNode + val root: MPSTreeNode? = projectTree?.rootNode if (root == null) { return } diff --git a/mps-legacy-sync-plugin/src/main/kotlin/org/modelix/mps/sync/ModelSyncProjectService.kt b/mps-legacy-sync-plugin/src/main/kotlin/org/modelix/mps/sync/ModelSyncProjectService.kt new file mode 100644 index 00000000..85885216 --- /dev/null +++ b/mps-legacy-sync-plugin/src/main/kotlin/org/modelix/mps/sync/ModelSyncProjectService.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.modelix.mps.sync + +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.Service +import com.intellij.openapi.project.Project +import org.modelix.model.mpsplugin.plugin.Mpsplugin_ProjectPlugin + +@Service(Service.Level.PROJECT) +class ModelSyncProjectService(val project: Project) : Disposable { + + private val legacyProjectPluginPart = Mpsplugin_ProjectPlugin() + + init { + legacyProjectPluginPart.init(project) + } + + override fun dispose() { + legacyProjectPluginPart.dispose() + } +} diff --git a/mps-legacy-sync-plugin/src/main/kotlin/org/modelix/mps/sync/ModelSyncService.kt b/mps-legacy-sync-plugin/src/main/kotlin/org/modelix/mps/sync/ModelSyncService.kt index 8a1ca3ea..3a5a694e 100644 --- a/mps-legacy-sync-plugin/src/main/kotlin/org/modelix/mps/sync/ModelSyncService.kt +++ b/mps-legacy-sync-plugin/src/main/kotlin/org/modelix/mps/sync/ModelSyncService.kt @@ -22,6 +22,7 @@ import io.ktor.client.HttpClient import io.ktor.http.Url import jetbrains.mps.project.MPSProject import kotlinx.coroutines.delay +import org.apache.log4j.LogManager import org.jetbrains.mps.openapi.model.SNode import org.jetbrains.mps.openapi.module.SModule import org.jetbrains.mps.openapi.project.Project @@ -56,15 +57,16 @@ import kotlin.time.ExperimentalTime class ModelSyncService : Disposable, ISyncService { companion object { + private val LOG = LogManager.getLogger(ModelSyncService::class.java) var INSTANCE: ModelSyncService? = null } - private var projects: Set = emptySet() private val legacyAppPluginParts = listOf( org.modelix.model.mpsadapters.plugin.ApplicationPlugin_AppPluginPart(), org.modelix.model.mpsplugin.plugin.ApplicationPlugin_AppPluginPart(), ) private var serverConnections: List = emptyList() + var httpClient: HttpClient? = null init { check(INSTANCE == null) { "Single instance expected" } @@ -84,11 +86,12 @@ class ModelSyncService : Disposable, ISyncService { } fun registerProject(project: com.intellij.openapi.project.Project) { - projects += project + project.getService(ModelSyncProjectService::class.java) } fun unregisterProject(project: com.intellij.openapi.project.Project) { - projects -= project + project.getService(ModelSyncProjectService::class.java) + .dispose() } override fun getBindings(): List { @@ -182,6 +185,13 @@ class ModelSyncService : Disposable, ISyncService { override fun dispose() { serverConnections -= this ModelServerConnections.instance.removeModelServer(legacyConnection) + try { + httpClient?.close() + httpClient = null + } catch (e: Throwable) { + LOG.error("Failed to close client.", e) + throw e + } } private inner class ActiveBranchAdapter(val repositoryId: RepositoryId, val legacyActiveBranch: ActiveBranch) : IBranchConnection { diff --git a/mps-legacy-sync-plugin/src/test/kotlin/PersistedBindingsLoadingTest.kt b/mps-legacy-sync-plugin/src/test/kotlin/PersistedBindingsLoadingTest.kt new file mode 100644 index 00000000..0344aee6 --- /dev/null +++ b/mps-legacy-sync-plugin/src/test/kotlin/PersistedBindingsLoadingTest.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import io.ktor.client.request.parameter +import io.ktor.client.request.post +import io.ktor.http.appendPathSegments +import io.ktor.http.takeFrom +import jetbrains.mps.smodel.SModelId +import org.modelix.model.api.BuiltinLanguages +import org.modelix.model.client2.ModelClientV2 +import org.modelix.model.mpsplugin.ModelServerConnections +import org.modelix.model.mpsplugin.SModuleUtils +import java.util.UUID + +class PersistedBindingsLoadingTest : SyncPluginTestBase("projectWithPersistedBindings") { + + fun `test persisted bindings are loaded initially`() = runTestWithSyncService { + // Arrange + delayUntil { + val modelServer = ModelServerConnections.instance.modelServers + .firstOrNull() + if (modelServer == null) { + return@delayUntil false + } + return@delayUntil modelServer.getRootBindings().isNotEmpty() + } + + // Act + val existingSolution = mpsProject.projectModules.single() + writeAction { + SModuleUtils.createModel( + existingSolution, + "my.wonderful.brandnew.modelInExistingModule", + SModelId.regular(UUID.fromString("1c22f2f9-f1f3-45f8-8f4b-69b248928af5")), + ) + } + + // Assert + delayUntil { + val dataOnServer = readDumpFromServer(defaultBranchRef) + val moduleData = dataOnServer.children.single() + val models = moduleData.children.filter { it.role == "models" } + models.size == 2 + } + + // XXX Project bindings do not close opened model server connections, + // because in the meantime they might be used by other project bindings. + // This should be solved by the new plugin. + val modelServers = ModelServerConnections.instance.modelServers.toList() + modelServers.forEach { + ModelServerConnections.instance.removeModelServer(it) + } + } + + override suspend fun postModelServerSetup() { + val moduleConcept = BuiltinLanguages.MPSRepositoryConcepts.Module + httpClient.post { + url { + takeFrom(baseUrl) + appendPathSegments("repositories", "${defaultBranchRef.repositoryId}", "init") + parameter("useRoleIds", "false") + } + } + ModelClientV2.builder().url(baseUrl).client(httpClient).build().use { client -> + client.init() + client.runWrite(defaultBranchRef) { root -> + val module = root.addNewChild("modules", -1, moduleConcept) + module.setPropertyValue("name", "NewSolution") + } + } + } +} diff --git a/mps-legacy-sync-plugin/src/test/kotlin/SyncPluginTestBase.kt b/mps-legacy-sync-plugin/src/test/kotlin/SyncPluginTestBase.kt index b092cac8..09b4369b 100644 --- a/mps-legacy-sync-plugin/src/test/kotlin/SyncPluginTestBase.kt +++ b/mps-legacy-sync-plugin/src/test/kotlin/SyncPluginTestBase.kt @@ -70,7 +70,7 @@ abstract class SyncPluginTestBase(private val testDataName: String?) : HeavyPlat suspend fun delayUntil( checkIntervalMilliseconds: Long = 1000, timeoutMilliseconds: Long = 30_000, - condition: () -> Boolean, + condition: suspend () -> Boolean, ) { check(checkIntervalMilliseconds > 0) { "checkIntervalMilliseconds must be positive." @@ -97,9 +97,10 @@ abstract class SyncPluginTestBase(private val testDataName: String?) : HeavyPlat protected lateinit var initialDumpFromMPS: NodeData protected val defaultBranchRef = RepositoryId("default").getBranchReference() - protected val mpsProject: MPSProject get() { - return checkNotNull(ProjectHelper.fromIdeaProject(project)) { "MPS project not loaded" } - } + protected val mpsProject: MPSProject + get() { + return checkNotNull(ProjectHelper.fromIdeaProject(project)) { "MPS project not loaded" } + } protected val projectAsNode: ProjectAsNode get() = org.modelix.model.mpsadapters.mps.ProjectAsNode(mpsProject) @@ -150,11 +151,16 @@ abstract class SyncPluginTestBase(private val testDataName: String?) : HeavyPlat KeyValueLikeModelServer(repositoriesManager).init(this) } httpClient = client + postModelServerSetup() block() } + open suspend fun postModelServerSetup() { + } + protected fun runTestWithSyncService(body: suspend (ISyncService) -> Unit) = runTestWithModelServer { val syncService = ApplicationManager.getApplication().getService(ModelSyncService::class.java) + syncService.httpClient = httpClient try { this@SyncPluginTestBase.syncService = syncService syncService.registerProject(project) @@ -176,7 +182,11 @@ abstract class SyncPluginTestBase(private val testDataName: String?) : HeavyPlat SetLibraryContributor.fromSet( "repositoryconcepts", setOf( - LibDescriptor(mpsProject.fileSystem.getFile(Path.of("repositoryconcepts").absolutePathString())), + LibDescriptor( + mpsProject.fileSystem.getFile( + Path.of("repositoryconcepts").absolutePathString(), + ), + ), ), ), ), @@ -248,7 +258,8 @@ abstract class SyncPluginTestBase(private val testDataName: String?) : HeavyPlat } protected fun resolveMPSConcept(languageName: String, conceptName: String): SAbstractConcept { - val baseLanguage = LanguageRegistry.getInstance(mpsProject.repository).allLanguages.single { it.qualifiedName == languageName } + val baseLanguage = + LanguageRegistry.getInstance(mpsProject.repository).allLanguages.single { it.qualifiedName == languageName } val classConcept = baseLanguage.concepts.single { it.name == conceptName } check(classConcept !is InvalidConcept) return classConcept @@ -284,6 +295,7 @@ private fun normalizeNodeData(node: NodeData, originalIds: MutableMap { // Module // TODO remove this filter and fix the test filteredChildren = filteredChildren.filter { it.role == "models" } @@ -295,6 +307,7 @@ private fun normalizeNodeData(node: NodeData, originalIds: MutableMap { // SingleLanguageDependency // TODO remove this filter and fix the test replacedId = null diff --git a/mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/.mps/.gitignore b/mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/.mps/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/.mps/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/.mps/cloudResources.xml b/mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/.mps/cloudResources.xml new file mode 100644 index 00000000..41603b46 --- /dev/null +++ b/mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/.mps/cloudResources.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/.mps/migration.xml b/mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/.mps/migration.xml new file mode 100644 index 00000000..d512ce45 --- /dev/null +++ b/mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/.mps/migration.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/.mps/modules.xml b/mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/.mps/modules.xml new file mode 100644 index 00000000..f31cd531 --- /dev/null +++ b/mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/.mps/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/.mps/vcs.xml b/mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/.mps/vcs.xml new file mode 100644 index 00000000..fbbc5665 --- /dev/null +++ b/mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/.mps/vcs.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/solutions/NewSolution/NewSolution.msd b/mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/solutions/NewSolution/NewSolution.msd new file mode 100644 index 00000000..ac346009 --- /dev/null +++ b/mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/solutions/NewSolution/NewSolution.msd @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/solutions/NewSolution/models/NewSolution.a_model.mps b/mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/solutions/NewSolution/models/NewSolution.a_model.mps new file mode 100644 index 00000000..09720b23 --- /dev/null +++ b/mps-legacy-sync-plugin/testdata/projectWithPersistedBindings/solutions/NewSolution/models/NewSolution.a_model.mps @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +