Skip to content

Commit d219a99

Browse files
committed
feat(mps-model-server-plugin): new IDEA plugin for running the LightModelServer
Currently, it's implemented in https://github.com/modelix/modelix/tree/mps/2020.3/mps/org.modelix.model.server.mpsplugin which publishes a separate version for each MPS version and has dependencies on MPS-extensions. The new plugin doesn't have any dependencies on MPS modules and there is only one version of the plugin for all MPS versions. This makes it much easier and faster to deploy changes. This is also one step towards the goal of MODELIX-177 to split the modelix/modelix repository into multiple repositories that don't have MPS version specific releases.
1 parent 60517cd commit d219a99

File tree

8 files changed

+344
-17
lines changed

8 files changed

+344
-17
lines changed

commitlint.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ module.exports = {
1616
"model-server",
1717
"model-sync-lib",
1818
"mps-model-adapters",
19+
"mps-model-server",
20+
"mps-model-server-plugin",
1921
"ts-model-api",
2022
],
2123
],

model-server-lib/src/main/kotlin/org/modelix/model/server/light/LightModelServer.kt

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,13 @@ import io.ktor.server.application.*
2121
import io.ktor.server.engine.*
2222
import io.ktor.server.netty.*
2323
import io.ktor.server.plugins.cors.routing.*
24+
import io.ktor.server.request.*
2425
import io.ktor.server.response.*
2526
import io.ktor.server.routing.*
2627
import io.ktor.server.websocket.*
27-
import io.ktor.util.*
2828
import io.ktor.websocket.*
2929
import kotlinx.coroutines.*
30-
import org.modelix.model.api.ConceptReference
31-
import org.modelix.model.api.IConceptReference
32-
import org.modelix.model.api.INode
33-
import org.modelix.model.api.INodeReference
34-
import org.modelix.model.api.INodeReferenceSerializer
35-
import org.modelix.model.api.IRole
36-
import org.modelix.model.api.key
37-
import org.modelix.model.api.remove
38-
import org.modelix.model.api.serialize
39-
import org.modelix.model.api.usesRoleIds
30+
import org.modelix.model.api.*
4031
import org.modelix.model.server.api.AddNewChildNodeOpData
4132
import org.modelix.model.server.api.ChangeSetId
4233
import org.modelix.model.server.api.DeleteNodeOpData
@@ -56,10 +47,49 @@ import java.time.Duration
5647
import java.util.*
5748
import kotlin.time.Duration.Companion.seconds
5849

59-
class LightModelServer @JvmOverloads constructor (val port: Int, val rootNode: INode, val ignoredRoles: Set<IRole> = emptySet(), additionalHealthChecks: List<IHealthCheck> = emptyList()) {
50+
class LightModelServerBuilder {
51+
private var port: Int = 48302
52+
private var rootNodeProvider: () -> INode? = { null }
53+
private var ignoredRoles: Set<IRole> = emptySet()
54+
private var additionalHealthChecks: List<LightModelServer.IHealthCheck> = emptyList()
55+
56+
fun port(port: Int): LightModelServerBuilder {
57+
this.port = port
58+
return this
59+
}
60+
61+
fun rootNode(provider: () -> INode?): LightModelServerBuilder {
62+
this.rootNodeProvider = provider
63+
return this
64+
}
65+
66+
fun rootNode(node: INode): LightModelServerBuilder {
67+
this.rootNodeProvider = { node }
68+
return this
69+
}
70+
71+
fun ignoreRole(role: IRole): LightModelServerBuilder {
72+
this.ignoredRoles += role
73+
return this
74+
}
75+
76+
fun healthCheck(check: LightModelServer.IHealthCheck): LightModelServerBuilder {
77+
this.additionalHealthChecks += check
78+
return this
79+
}
80+
81+
fun build(): LightModelServer {
82+
return LightModelServer(port, rootNodeProvider, ignoredRoles, additionalHealthChecks)
83+
}
84+
}
85+
86+
class LightModelServer @JvmOverloads constructor (val port: Int, val rootNodeProvider: () -> INode?, val ignoredRoles: Set<IRole> = emptySet(), additionalHealthChecks: List<IHealthCheck> = emptyList()) {
87+
constructor (port: Int, rootNode: INode, ignoredRoles: Set<IRole> = emptySet(), additionalHealthChecks: List<IHealthCheck> = emptyList()) :
88+
this(port, { rootNode}, ignoredRoles, additionalHealthChecks)
6089

6190
companion object {
6291
private val LOG = mu.KotlinLogging.logger { }
92+
fun builder(): LightModelServerBuilder = LightModelServerBuilder()
6393
}
6494

6595
private var server: NettyApplicationEngine? = null
@@ -70,18 +100,26 @@ class LightModelServer @JvmOverloads constructor (val port: Int, val rootNode: I
70100
override val enabledByDefault: Boolean = true
71101

72102
override fun run(output: StringBuilder): Boolean {
103+
val n = rootNodeProvider()
104+
if (n == null) {
105+
output.appendLine("root node not available yet")
106+
return false
107+
}
73108
val count = getArea().executeRead { rootNode.allChildren.count() }
74109
output.appendLine("root node has $count children")
75110
return true
76111
}
77112
}) + additionalHealthChecks
78113

79-
fun start() {
114+
val rootNode: INode get() = rootNodeProvider() ?: throw IllegalStateException("Root node not available yet")
115+
116+
@JvmOverloads
117+
fun start(wait: Boolean = false) {
80118
LOG.trace { "server starting on port $port ..." }
81119
server = embeddedServer(Netty, port = port) {
82-
init()
120+
installHandlers()
83121
}
84-
server!!.start()
122+
server!!.start(wait)
85123
LOG.trace { "server started" }
86124
}
87125

@@ -116,7 +154,7 @@ class LightModelServer @JvmOverloads constructor (val port: Int, val rootNode: I
116154

117155
private fun getArea() = rootNode.getArea()
118156

119-
private fun Application.init() {
157+
fun Application.installHandlers() {
120158
install(WebSockets) {
121159
pingPeriod = Duration.ofSeconds(15)
122160
timeout = Duration.ofSeconds(15)
@@ -169,7 +207,7 @@ class LightModelServer @JvmOverloads constructor (val port: Int, val rootNode: I
169207
} else {
170208
call.respond(HttpStatusCode.InternalServerError, "unhealthy\n\n$output")
171209
}
172-
} catch (ex: Exception) {
210+
} catch (ex: Throwable) {
173211
output.appendLine()
174212
output.appendLine(ex.stackTraceToString())
175213
call.respond(HttpStatusCode.InternalServerError, "unhealthy\n\n$output")

mps-model-server-plugin/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
To test changes during development
2+
add the property `mps.plugins.dir` to `~/.gradle.gradle.properties`, for example
3+
when using the JetBrains toolbox on Mac this would be similar to this:
4+
```
5+
mps.plugins.dir=/Users/yourUserName/Library/Application Support/JetBrains/Toolbox/apps/MPS/ch-2/211.7628.1509/MPS 2021.1.app.plugins/
6+
```
7+
8+
Then run the task `installMpsPlugin` and restart MPS.
9+
Automatically reloading the plugin is not supported yet,
10+
because failing to unloading the classes of the ktor server prevents that.
11+
12+
Alternatively you can install the plugin manually by first running the task `buildPlugin`
13+
and then choosing the folder `mps-model-server/build/distributions/` in MPS.
14+
15+
To execute a query you can create a Kotlin scratch file with the classpath
16+
of the module `modelql-client.jvmMain` and some code like this:
17+
18+
```kotlin
19+
import kotlinx.coroutines.runBlocking
20+
import org.modelix.modelql.client.ModelQLClient
21+
import org.modelix.modelql.core.count
22+
import org.modelix.modelql.untyped.children
23+
24+
val client = ModelQLClient.builder().url("http://localhost:48305/query").build()
25+
val result = runBlocking {
26+
client.query {
27+
it.children("modules").count()
28+
}
29+
}
30+
println(result)
31+
```
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
plugins {
2+
id("org.jetbrains.kotlin.jvm")
3+
id("org.jetbrains.intellij") version "1.13.3"
4+
alias(libs.plugins.ktlint)
5+
}
6+
7+
dependencies {
8+
implementation(project(":model-server-lib"))
9+
implementation(project(":mps-model-adapters"))
10+
compileOnly("com.jetbrains:mps-openapi:2021.1.4")
11+
compileOnly("com.jetbrains:mps-core:2021.1.4")
12+
compileOnly("com.jetbrains:mps-environment:2021.1.4")
13+
}
14+
15+
// Configure Gradle IntelliJ Plugin
16+
// Read more: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html
17+
intellij {
18+
19+
// IDEA platform version used in MPS 2021.1.4: https://github.com/JetBrains/MPS/blob/2021.1.4/build/version.properties#L11
20+
version.set("211.7628.21")
21+
22+
// type.set("IC") // Target IDE Platform
23+
24+
// plugins.set(listOf("jetbrains.mps.core", "com.intellij.modules.mps"))
25+
}
26+
27+
tasks {
28+
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
29+
kotlinOptions.jvmTarget = "11"
30+
}
31+
32+
patchPluginXml {
33+
sinceBuild.set("211")
34+
untilBuild.set("231.*")
35+
}
36+
37+
buildSearchableOptions {
38+
enabled = false
39+
}
40+
41+
// signPlugin {
42+
// certificateChain.set(System.getenv("CERTIFICATE_CHAIN"))
43+
// privateKey.set(System.getenv("PRIVATE_KEY"))
44+
// password.set(System.getenv("PRIVATE_KEY_PASSWORD"))
45+
// }
46+
//
47+
// publishPlugin {
48+
// token.set(System.getenv("PUBLISH_TOKEN"))
49+
// }
50+
51+
runIde {
52+
autoReloadPlugins.set(true)
53+
}
54+
55+
val mpsPluginDir = project.findProperty("mps.plugins.dir")?.toString()?.let { file(it) }
56+
if (mpsPluginDir != null && mpsPluginDir.isDirectory) {
57+
create<Sync>("installMpsPlugin") {
58+
dependsOn(prepareSandbox)
59+
from(buildDir.resolve("idea-sandbox/plugins/mps-model-server-plugin"))
60+
into(mpsPluginDir.resolve("mps-model-server-plugin"))
61+
}
62+
}
63+
}
64+
65+
group = "org.modelix.mps"
66+
67+
publishing {
68+
publications {
69+
create<MavenPublication>("maven") {
70+
artifactId = "model-server-plugin"
71+
artifact(tasks.buildPlugin) {
72+
extension = "zip"
73+
}
74+
}
75+
}
76+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package org.modelix.model.server.mps
15+
16+
import com.intellij.ide.AppLifecycleListener
17+
import com.intellij.ide.plugins.DynamicPluginListener
18+
import com.intellij.ide.plugins.IdeaPluginDescriptor
19+
import com.intellij.openapi.Disposable
20+
import com.intellij.openapi.components.Service
21+
import com.intellij.openapi.components.service
22+
import com.intellij.openapi.project.Project
23+
import jetbrains.mps.project.ProjectBase
24+
import jetbrains.mps.project.ProjectManager
25+
import jetbrains.mps.smodel.MPSModuleRepository
26+
import org.modelix.model.api.INode
27+
import org.modelix.model.api.runSynchronized
28+
import org.modelix.model.mpsadapters.MPSRepositoryAsNode
29+
import org.modelix.model.server.light.LightModelServer
30+
31+
@Service(Service.Level.APP)
32+
class MPSModelServer : Disposable {
33+
init {
34+
println("modelix server created")
35+
}
36+
37+
private var server: LightModelServer? = null
38+
39+
fun ensureStarted() {
40+
runSynchronized(this) {
41+
if (server != null) return
42+
43+
println("starting modelix server")
44+
45+
val rootNodeProvider: () -> INode? = { MPSModuleRepository.getInstance()?.let { MPSRepositoryAsNode(it) } }
46+
server = LightModelServer.builder()
47+
.port(48305)
48+
.rootNode(rootNodeProvider)
49+
.healthCheck(object : LightModelServer.IHealthCheck {
50+
override val id: String
51+
get() = "projects"
52+
override val enabledByDefault: Boolean
53+
get() = false
54+
55+
override fun run(output: StringBuilder): Boolean {
56+
val projects = ProjectManager.getInstance().openedProjects
57+
output.append("${projects.size} projects found")
58+
projects.forEach { output.append(" ${it.name}") }
59+
return ProjectManager.getInstance().openedProjects.isNotEmpty()
60+
}
61+
})
62+
.healthCheck(object : LightModelServer.IHealthCheck {
63+
override val id: String
64+
get() = "virtualFolders"
65+
override val enabledByDefault: Boolean
66+
get() = false
67+
68+
override fun run(output: StringBuilder): Boolean {
69+
val projects = ProjectManager.getInstance().openedProjects.filterIsInstance<ProjectBase>()
70+
for (project in projects) {
71+
val modules = project.projectModules
72+
val virtualFolders = modules
73+
.mapNotNull { project.getPath(it)?.virtualFolder }
74+
.filter { it.isNotEmpty() }
75+
output.append("project ${project.name} contains ${modules.size} modules with ${virtualFolders.size} virtual folders")
76+
if (virtualFolders.isNotEmpty()) return true
77+
}
78+
return false
79+
}
80+
})
81+
.build()
82+
server!!.start()
83+
}
84+
}
85+
86+
fun ensureStopped() {
87+
runSynchronized(this) {
88+
if (server == null) return
89+
println("stopping modelix server")
90+
server?.stop()
91+
server = null
92+
}
93+
}
94+
95+
override fun dispose() {
96+
ensureStopped()
97+
}
98+
}
99+
100+
class MPSModelServerDynamicPluginListener : DynamicPluginListener {
101+
override fun pluginLoaded(pluginDescriptor: IdeaPluginDescriptor) {
102+
service<MPSModelServer>().ensureStarted()
103+
}
104+
}
105+
106+
class MPSModelServerAppLifecycleListener : AppLifecycleListener {
107+
override fun appStarting(projectFromCommandLine: Project?) {
108+
service<MPSModelServer>().ensureStarted()
109+
}
110+
111+
override fun appStarted() {
112+
service<MPSModelServer>().ensureStarted()
113+
}
114+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<!-- Plugin Configuration File. Read more: https://plugins.jetbrains.com/docs/intellij/plugin-configuration-file.html -->
2+
<idea-plugin require-restart="true">
3+
<!-- Unique identifier of the plugin. It should be FQN. It cannot be changed between the plugin versions. -->
4+
<id>org.modelix.model.server.mps</id>
5+
6+
<!-- Public plugin name should be written in Title Case.
7+
Guidelines: https://plugins.jetbrains.com/docs/marketplace/plugin-overview-page.html#plugin-name -->
8+
<name>MPS as Modelix Model Server</name>
9+
10+
<!-- A displayed Vendor name or Organization ID displayed on the Plugins Page. -->
11+
<vendor email="[email protected]" url="https://modelix.org/">itemis AG</vendor>
12+
13+
<!-- Description of the plugin displayed on the Plugin Page and IDE Plugin Manager.
14+
Simple HTML elements (text formatting, paragraphs, and lists) can be added inside of <![CDATA[ ]]> tag.
15+
Guidelines: https://plugins.jetbrains.com/docs/marketplace/plugin-overview-page.html#plugin-description -->
16+
<description><![CDATA[
17+
Modelix compatible model server for MPS models
18+
]]></description>
19+
20+
<!-- Product and plugin compatibility requirements.
21+
Read more: https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html -->
22+
<depends>com.intellij.modules.platform</depends>
23+
24+
<applicationListeners>
25+
<listener
26+
class="org.modelix.model.server.mps.MPSModelServerDynamicPluginListener"
27+
topic="com.intellij.ide.plugins.DynamicPluginListener"/>
28+
<listener
29+
class="org.modelix.model.server.mps.MPSModelServerAppLifecycleListener"
30+
topic="com.intellij.ide.AppLifecycleListener"/>
31+
</applicationListeners>
32+
33+
<!-- Extension points defined by the plugin.
34+
Read more: https://plugins.jetbrains.com/docs/intellij/plugin-extension-points.html -->
35+
<extensions defaultExtensionNs="com.intellij">
36+
37+
</extensions>
38+
</idea-plugin>

0 commit comments

Comments
 (0)