From e43e7bc07951a91d3961fcbc2f5b2c5ffca716eb Mon Sep 17 00:00:00 2001 From: Yaohui Wang Date: Wed, 29 Jun 2022 18:45:58 +0800 Subject: [PATCH] [jb] check maxHeapSize of IDE server and guide users to set vmoptions in .gitpod.yml - Register a background activity (GitpodStartupActivity) for handling postStart events - Add YAML plugin dependency for parsing & navigating PSI elements (AST) in .gitpod.yml - Show warning notification when the runtime Xmx is different from the configured Xmx - Add edit/create-if-missing .gitpod.yml quick actions, and navigate caret to `vmoptions` YAML key if exists - Add help action for browsing gitpod.yml references - Set -Xmx for the `runIde` task (only affect local sandbox IDE instance with the developed plugin), which can be modified for local testing purpose --- .../jetbrains/backend-plugin/build.gradle.kts | 4 + .../backend-plugin/gradle.properties | 2 +- .../jetbrains/remote/GitpodStartupActivity.kt | 129 ++++++++++++++++++ .../jetbrains/remote/utils/GitpodConfig.kt | 45 ++++++ .../src/main/resources/META-INF/plugin.xml | 2 + 5 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodStartupActivity.kt create mode 100644 components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/utils/GitpodConfig.kt diff --git a/components/ide/jetbrains/backend-plugin/build.gradle.kts b/components/ide/jetbrains/backend-plugin/build.gradle.kts index 3769309c2c9ba2..af1348a3bd2f80 100644 --- a/components/ide/jetbrains/backend-plugin/build.gradle.kts +++ b/components/ide/jetbrains/backend-plugin/build.gradle.kts @@ -111,6 +111,10 @@ tasks { useJUnitPlatform() } + runIde { + jvmArgs = listOf("-Xmx2096m") + } + runPluginVerifier { ideVersions.set(properties("pluginVerifierIdeVersions").split(',').map(String::trim).filter(String::isNotEmpty)) } diff --git a/components/ide/jetbrains/backend-plugin/gradle.properties b/components/ide/jetbrains/backend-plugin/gradle.properties index 2cbce2c7b77a77..846a70cb13f697 100644 --- a/components/ide/jetbrains/backend-plugin/gradle.properties +++ b/components/ide/jetbrains/backend-plugin/gradle.properties @@ -10,7 +10,7 @@ platformType=IU platformDownloadSources=true # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 -platformPlugins=Git4Idea, org.jetbrains.plugins.terminal, com.jetbrains.codeWithMe +platformPlugins=Git4Idea, org.jetbrains.plugins.terminal, com.jetbrains.codeWithMe, org.jetbrains.plugins.yaml # Opt-out flag for bundling Kotlin standard library. # See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details. kotlin.stdlib.default.dependency=false diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodStartupActivity.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodStartupActivity.kt new file mode 100644 index 00000000000000..e9b2270a4dc809 --- /dev/null +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodStartupActivity.kt @@ -0,0 +1,129 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.jetbrains.remote + +import com.intellij.diagnostic.VMOptions +import com.intellij.ide.BrowserUtil +import com.intellij.ide.actions.OpenFileAction +import com.intellij.notification.NotificationAction +import com.intellij.notification.NotificationGroupManager +import com.intellij.notification.NotificationType +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.StartupActivity +import com.intellij.openapi.util.BuildNumber +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiFileFactory +import com.intellij.psi.PsiManager +import com.intellij.util.application +import io.gitpod.jetbrains.remote.utils.GitpodConfig +import io.gitpod.jetbrains.remote.utils.GitpodConfig.YamlKey.jetbrains +import io.gitpod.jetbrains.remote.utils.GitpodConfig.YamlKey.vmOptions +import io.gitpod.jetbrains.remote.utils.GitpodConfig.defaultXmxOption +import io.gitpod.jetbrains.remote.utils.GitpodConfig.gitpodYamlFile +import io.gitpod.jetbrains.remote.utils.GitpodConfig.gitpodYamlReferenceLink +import org.jetbrains.yaml.YAMLFileType +import org.jetbrains.yaml.YAMLUtil +import org.jetbrains.yaml.psi.YAMLFile +import org.jetbrains.yaml.psi.YAMLKeyValue +import java.nio.file.Paths + +class GitpodStartupActivity : StartupActivity.Background { + + override fun runActivity(project: Project) { + application.invokeLater { checkMemoryOption(project) } + } + + private fun checkMemoryOption(project: Project) { + val productCode = BuildNumber.currentVersion().productCode + val productName = GitpodConfig.getJetBrainsProductName(productCode) ?: return + // it's ok for .gitpod.yml to not exist + var vmOptions = emptyList() + getGitpodYamlVirtualFile(project)?.let { + val vmOptionsKeyValue = getGitpodYamlVMOptionsPsiElement(project, it, productName) + vmOptions = vmOptionsKeyValue?.valueText?.split("\\s".toRegex()) ?: emptyList() + } + // if there is no -Xmx option from .gitpod.yml, compare runtime maxHeapSize with default value + var xmxVmOptions = vmOptions.filter { it.startsWith("-Xmx") } + if (xmxVmOptions.isEmpty()) { + xmxVmOptions = listOf(defaultXmxOption) + } + // the rightmost -Xmx option is the one to take effect (after deduplication) + val finalXmx = xmxVmOptions.last() + // shift right 20 (xmxInBytes >> 20) to convert to MiB + val finalXmxValueInMiB = VMOptions.parseMemoryOption(finalXmx.substringAfter("-Xmx")).shr(20) + val runtimeXmxValueInMiB = Runtime.getRuntime().maxMemory().shr(20) + if (finalXmxValueInMiB != runtimeXmxValueInMiB) { + showEditVMOptionsNotification(project, runtimeXmxValueInMiB, finalXmxValueInMiB, productName) + } + } + + private fun getGitpodYamlVirtualFile(project: Project): VirtualFile? { + val basePath = project.basePath ?: return null + return VfsUtil.findFile(Paths.get(basePath, gitpodYamlFile), true) + } + + private fun getGitpodYamlVMOptionsPsiElement( + project: Project, + virtualFile: VirtualFile, + productName: String + ): YAMLKeyValue? { + val psiFile = PsiManager.getInstance(project).findFile(virtualFile) as? YAMLFile ?: return null + return YAMLUtil.getQualifiedKeyInFile(psiFile, jetbrains, productName, vmOptions) + } + + private fun showEditVMOptionsNotification(project: Project, runtimeXmxMiB: Long, configXmxMiB: Long, productName: String) { + val notificationGroup = NotificationGroupManager.getInstance().getNotificationGroup("Gitpod Notifications") + val title = "Gitpod memory settings" + val message = """ + |Current maxHeapSize -Xmx${runtimeXmxMiB}m is not matched to configured -Xmx${configXmxMiB}m.
+ |Set vmoptions in .gitpod.yml + """.trimMargin() + val notification = notificationGroup.createNotification(title, message, NotificationType.WARNING) + // edit or create .gitpod.yaml + val virtualFile = getGitpodYamlVirtualFile(project) + val primaryAction = if (virtualFile != null) { + editGitpodYamlAction(project, virtualFile, productName) + } else { + createGitpodYamlAction(project, productName, runtimeXmxMiB) + } + notification.addAction(primaryAction) + // show gitpod.yml reference + val helpAction = NotificationAction.createSimple("More info") { + BrowserUtil.browse(gitpodYamlReferenceLink) + } + notification.addAction(helpAction) + notification.notify(project) + } + + private fun editGitpodYamlAction(project: Project, gitpodYaml: VirtualFile, productName: String): NotificationAction { + return NotificationAction.createSimple("Edit .gitpod.yml") { + OpenFileAction.openFile(gitpodYaml, project) + val vmOptionsKeyValue = getGitpodYamlVMOptionsPsiElement(project, gitpodYaml, productName) + // navigate caret to "vmoptions" if exist + vmOptionsKeyValue?.navigate(true) + } + } + + private fun createGitpodYamlAction(project: Project, productName: String, runtimeXmxMiB: Long): NotificationAction { + return NotificationAction.createSimple("Create .gitpod.yml") { + application.runWriteAction { + val psiFile = PsiFileFactory.getInstance(project).createFileFromText( + gitpodYamlFile, + YAMLFileType.YML, + GitpodConfig.YamlTemplate.buildVMOptions(productName, runtimeXmxMiB) + ) + project.basePath?.let { basePath -> + LocalFileSystem.getInstance().findFileByPath(basePath)?.let { dir -> + PsiManager.getInstance(project).findDirectory(dir)?.add(psiFile) + // refresh VFS and open created .gitpod.yml in editor + getGitpodYamlVirtualFile(project)?.let { OpenFileAction.openFile(it, project) } + } + } + } + } + } +} diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/utils/GitpodConfig.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/utils/GitpodConfig.kt new file mode 100644 index 00000000000000..0620b18a2f4654 --- /dev/null +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/utils/GitpodConfig.kt @@ -0,0 +1,45 @@ +// Copyright (c) 2021 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.jetbrains.remote.utils + +/** + * Constants and util functions for Gitpod config spec + */ +object GitpodConfig { + + const val gitpodYamlFile = ".gitpod.yml" + const val defaultXmxOption = "-Xmx2048m" + const val gitpodYamlReferenceLink = "https://www.gitpod.io/docs/references/gitpod-yml#jetbrainsproductvmoptions" + + object YamlKey { + const val jetbrains = "jetbrains" + const val vmOptions = "vmoptions" + } + + object YamlTemplate { + + fun buildVMOptions(productName: String, xmxValueMiB: Long): String { + return """ + |jetbrains: + | $productName: + | vmoptions: "-Xmx${xmxValueMiB}m" + """.trimMargin() + } + } + + /** + * map JetBrains IDE productCode to YAML key for .gitpod.yml + */ + fun getJetBrainsProductName(productCode: String): String? { + return when (productCode) { + "IC" -> "intellij" + "IU" -> "intellij" + "PS" -> "phpstorm" + "PY" -> "pycharm" + "GO" -> "goland" + else -> null + } + } +} diff --git a/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml b/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml index ca19490a92ab7b..932c373256e9a7 100644 --- a/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml +++ b/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml @@ -19,9 +19,11 @@ + +