Skip to content

Commit

Permalink
fix(mps-legacy-sync-plugin): use legacy plugin parts
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Oleksandr Dzhychko committed Mar 5, 2024
1 parent d9bcc94 commit 8fb19c8
Show file tree
Hide file tree
Showing 14 changed files with 277 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ->
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProjectTree?> {
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<in ProjectTree>) {
Expand All @@ -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
}
}

Expand All @@ -164,6 +168,7 @@ class CloudProjectViewExtension(private val project: Project?) {
}

fun dispose() {
waitForProjectTreeTimer?.stop()
project!!.repository.removeRepositoryListener(repositoryListener)
if (treeModel != null) {
treeModel!!.removeTreeModelListener(treeListener)
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<com.intellij.openapi.project.Project> = emptySet()
private val legacyAppPluginParts = listOf(
org.modelix.model.mpsadapters.plugin.ApplicationPlugin_AppPluginPart(),
org.modelix.model.mpsplugin.plugin.ApplicationPlugin_AppPluginPart(),
)
private var serverConnections: List<ServerConnection> = emptyList()
var httpClient: HttpClient? = null

init {
check(INSTANCE == null) { "Single instance expected" }
Expand All @@ -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<org.modelix.mps.sync.api.IBinding> {
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
}
}
}
}
25 changes: 19 additions & 6 deletions mps-legacy-sync-plugin/src/test/kotlin/SyncPluginTestBase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand All @@ -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)

Expand Down Expand Up @@ -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)
Expand All @@ -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(),
),
),
),
),
),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -284,6 +295,7 @@ private fun normalizeNodeData(node: NodeData, originalIds: MutableMap<String, St
filteredProperties -= "name"
filteredProperties -= BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name.getUID()
}

"mps:0a7577d1-d4e5-431d-98b1-fae38f9aee80/474657388638618895" -> { // Module
// TODO remove this filter and fix the test
filteredChildren = filteredChildren.filter { it.role == "models" }
Expand All @@ -295,6 +307,7 @@ private fun normalizeNodeData(node: NodeData, originalIds: MutableMap<String, St
replacedId = "mps-module:" + node.properties["id"] + "(" + node.properties["name"] + ")"
}
}

"mps:0a7577d1-d4e5-431d-98b1-fae38f9aee80/2206727074858242429" -> { // SingleLanguageDependency
// TODO remove this filter and fix the test
replacedId = null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CloudResources">
<option name="mappedModules">
<set>
<option value="http://localhost/default#NewSolution" />
</set>
</option>
<option name="modelServers">
<set>
<option value="http://localhost/" />
</set>
</option>
</component>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MigrationProperties">
<entry key="project.baseline.version" value="211" />
</component>
</project>
Loading

0 comments on commit 8fb19c8

Please sign in to comment.