|
| 1 | +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. |
| 2 | +// Licensed under the GNU Affero General Public License (AGPL). |
| 3 | +// See License-AGPL.txt in the project root for license information. |
| 4 | + |
| 5 | +package io.gitpod.jetbrains.remote |
| 6 | + |
| 7 | +import com.intellij.diagnostic.VMOptions |
| 8 | +import com.intellij.ide.BrowserUtil |
| 9 | +import com.intellij.ide.actions.OpenFileAction |
| 10 | +import com.intellij.notification.NotificationAction |
| 11 | +import com.intellij.notification.NotificationGroupManager |
| 12 | +import com.intellij.notification.NotificationType |
| 13 | +import com.intellij.openapi.project.Project |
| 14 | +import com.intellij.openapi.startup.StartupActivity |
| 15 | +import com.intellij.openapi.util.BuildNumber |
| 16 | +import com.intellij.openapi.vfs.LocalFileSystem |
| 17 | +import com.intellij.openapi.vfs.VfsUtil |
| 18 | +import com.intellij.openapi.vfs.VirtualFile |
| 19 | +import com.intellij.psi.PsiFileFactory |
| 20 | +import com.intellij.psi.PsiManager |
| 21 | +import com.intellij.util.application |
| 22 | +import io.gitpod.jetbrains.remote.utils.GitpodConfig |
| 23 | +import io.gitpod.jetbrains.remote.utils.GitpodConfig.YamlKey.jetbrains |
| 24 | +import io.gitpod.jetbrains.remote.utils.GitpodConfig.YamlKey.vmOptions |
| 25 | +import io.gitpod.jetbrains.remote.utils.GitpodConfig.defaultXmxOption |
| 26 | +import io.gitpod.jetbrains.remote.utils.GitpodConfig.gitpodYamlFile |
| 27 | +import io.gitpod.jetbrains.remote.utils.GitpodConfig.gitpodYamlReferenceLink |
| 28 | +import org.jetbrains.yaml.YAMLFileType |
| 29 | +import org.jetbrains.yaml.YAMLUtil |
| 30 | +import org.jetbrains.yaml.psi.YAMLFile |
| 31 | +import org.jetbrains.yaml.psi.YAMLKeyValue |
| 32 | +import java.nio.file.Paths |
| 33 | + |
| 34 | +class GitpodStartupActivity : StartupActivity.Background { |
| 35 | + |
| 36 | + override fun runActivity(project: Project) { |
| 37 | + application.invokeLater { checkMemoryOption(project) } |
| 38 | + } |
| 39 | + |
| 40 | + private fun checkMemoryOption(project: Project) { |
| 41 | + val productCode = BuildNumber.currentVersion().productCode |
| 42 | + val productName = GitpodConfig.getJetBrainsProductName(productCode) ?: return |
| 43 | + // it's ok for .gitpod.yml to not exist |
| 44 | + var vmOptions = emptyList<String>() |
| 45 | + getGitpodYamlVirtualFile(project)?.let { |
| 46 | + val vmOptionsKeyValue = getGitpodYamlVMOptionsPsiElement(project, it, productName) |
| 47 | + vmOptions = vmOptionsKeyValue?.valueText?.split("\\s".toRegex()) ?: emptyList() |
| 48 | + } |
| 49 | + // if there is no -Xmx option from .gitpod.yml, compare runtime maxHeapSize with default value |
| 50 | + var xmxVmOptions = vmOptions.filter { it.startsWith("-Xmx") } |
| 51 | + if (xmxVmOptions.isEmpty()) { |
| 52 | + xmxVmOptions = listOf(defaultXmxOption) |
| 53 | + } |
| 54 | + // the rightmost -Xmx option is the one to take effect (after deduplication) |
| 55 | + val finalXmx = xmxVmOptions.last() |
| 56 | + // shift right 20 (xmxInBytes >> 20) to convert to MiB |
| 57 | + val finalXmxValueInMiB = VMOptions.parseMemoryOption(finalXmx.substringAfter("-Xmx")).shr(20) |
| 58 | + val runtimeXmxValueInMiB = Runtime.getRuntime().maxMemory().shr(20) |
| 59 | + if (finalXmxValueInMiB != runtimeXmxValueInMiB) { |
| 60 | + showEditVMOptionsNotification(project, runtimeXmxValueInMiB, finalXmxValueInMiB, productName) |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + private fun getGitpodYamlVirtualFile(project: Project): VirtualFile? { |
| 65 | + val basePath = project.basePath ?: return null |
| 66 | + return VfsUtil.findFile(Paths.get(basePath, gitpodYamlFile), true) |
| 67 | + } |
| 68 | + |
| 69 | + private fun getGitpodYamlVMOptionsPsiElement( |
| 70 | + project: Project, |
| 71 | + virtualFile: VirtualFile, |
| 72 | + productName: String |
| 73 | + ): YAMLKeyValue? { |
| 74 | + val psiFile = PsiManager.getInstance(project).findFile(virtualFile) as? YAMLFile ?: return null |
| 75 | + return YAMLUtil.getQualifiedKeyInFile(psiFile, jetbrains, productName, vmOptions) |
| 76 | + } |
| 77 | + |
| 78 | + private fun showEditVMOptionsNotification(project: Project, runtimeXmxMiB: Long, configXmxMiB: Long, productName: String) { |
| 79 | + val notificationGroup = NotificationGroupManager.getInstance().getNotificationGroup("Gitpod Notifications") |
| 80 | + val title = "Gitpod memory settings" |
| 81 | + val message = """ |
| 82 | + |Current maxHeapSize <code>-Xmx${runtimeXmxMiB}m</code> is not matched to configured <code>-Xmx${configXmxMiB}m</code>.<br/> |
| 83 | + |Set vmoptions in .gitpod.yml |
| 84 | + """.trimMargin() |
| 85 | + val notification = notificationGroup.createNotification(title, message, NotificationType.WARNING) |
| 86 | + // edit or create .gitpod.yaml |
| 87 | + val virtualFile = getGitpodYamlVirtualFile(project) |
| 88 | + val primaryAction = if (virtualFile != null) { |
| 89 | + editGitpodYamlAction(project, virtualFile, productName) |
| 90 | + } else { |
| 91 | + createGitpodYamlAction(project, productName, runtimeXmxMiB) |
| 92 | + } |
| 93 | + notification.addAction(primaryAction) |
| 94 | + // show gitpod.yml reference |
| 95 | + val helpAction = NotificationAction.createSimple("More info") { |
| 96 | + BrowserUtil.browse(gitpodYamlReferenceLink) |
| 97 | + } |
| 98 | + notification.addAction(helpAction) |
| 99 | + notification.notify(project) |
| 100 | + } |
| 101 | + |
| 102 | + private fun editGitpodYamlAction(project: Project, gitpodYaml: VirtualFile, productName: String): NotificationAction { |
| 103 | + return NotificationAction.createSimple("Edit .gitpod.yml") { |
| 104 | + OpenFileAction.openFile(gitpodYaml, project) |
| 105 | + val vmOptionsKeyValue = getGitpodYamlVMOptionsPsiElement(project, gitpodYaml, productName) |
| 106 | + // navigate caret to "vmoptions" if exist |
| 107 | + vmOptionsKeyValue?.navigate(true) |
| 108 | + } |
| 109 | + } |
| 110 | + |
| 111 | + private fun createGitpodYamlAction(project: Project, productName: String, runtimeXmxMiB: Long): NotificationAction { |
| 112 | + return NotificationAction.createSimple("Create .gitpod.yml") { |
| 113 | + application.runWriteAction { |
| 114 | + val psiFile = PsiFileFactory.getInstance(project).createFileFromText( |
| 115 | + gitpodYamlFile, |
| 116 | + YAMLFileType.YML, |
| 117 | + GitpodConfig.YamlTemplate.buildVMOptions(productName, runtimeXmxMiB) |
| 118 | + ) |
| 119 | + project.basePath?.let { basePath -> |
| 120 | + LocalFileSystem.getInstance().findFileByPath(basePath)?.let { dir -> |
| 121 | + PsiManager.getInstance(project).findDirectory(dir)?.add(psiFile) |
| 122 | + // refresh VFS and open created .gitpod.yml in editor |
| 123 | + getGitpodYamlVirtualFile(project)?.let { OpenFileAction.openFile(it, project) } |
| 124 | + } |
| 125 | + } |
| 126 | + } |
| 127 | + } |
| 128 | + } |
| 129 | +} |
0 commit comments