From ad72df9520f954aec8b509d09ca1a5c579ae1688 Mon Sep 17 00:00:00 2001 From: SerVB Date: Fri, 16 Oct 2020 19:46:43 +0300 Subject: [PATCH] PRJ-129 Attach agent dynamically --- projector-agent/README.md | 34 +++-- projector-agent/build.gradle | 30 +++-- .../projector/agent/AgentLauncher.kt | 74 +++++++++++ .../projector/agent/CommandsHandler.kt | 2 +- .../projector/agent/GraphicsInterceptor.kt | 7 +- .../projector/agent/GraphicsState.kt | 2 +- .../projector/agent/GraphicsTransformer.kt | 8 +- .../jetbrains/projector/agent/MainAgent.kt | 33 +++-- projector-plugin/README.md | 12 +- projector-plugin/build.gradle.kts | 6 +- .../src/main/kotlin/ActivateAction.kt | 33 +++++ .../src/main/kotlin/CopyUrlAction.kt | 6 +- .../src/main/kotlin/DisableAction.kt | 8 +- .../src/main/kotlin/EnableAction.kt | 8 +- .../src/main/kotlin/ProjectorService.kt | 123 ++++++++---------- .../RegisterPluginInstallerStateListener.kt | 4 +- .../src/main/resources/META-INF/plugin.xml | 3 +- .../projector/server/ProjectorServer.kt | 13 +- .../projector/server/util/FontCacher.kt | 6 +- 19 files changed, 272 insertions(+), 140 deletions(-) create mode 100644 projector-agent/src/main/kotlin/org/jetbrains/projector/agent/AgentLauncher.kt create mode 100644 projector-plugin/src/main/kotlin/ActivateAction.kt diff --git a/projector-agent/README.md b/projector-agent/README.md index 66e260b7..6e1b314b 100644 --- a/projector-agent/README.md +++ b/projector-agent/README.md @@ -1,29 +1,35 @@ # projector-agent -This subproject allows draw application window and send it with server at the same time. +This subproject allows draw application window and send it with projector-server at the same time. ## Building -This will give you a jar: - +This will give you a jar in the `projector-agent/build/libs` dir: ```shell script ./gradlew :projector-agent:jar ``` -This command creates a jar in the `projector-agent/build/libs` dir. - ## How to run my application using this? +### Not modifying your application code To launch your app, change your run script like this: ```Shell Script java \ --javaagent:PATH_TO_AGENT_JAR=PATH_TO_AGENT_JAR \ -YOUR_USUAL_JAVA_ARGUMENTS +-Djdk.attach.allowAttachSelf=true \ +-Dswing.bufferPerWindow=false \ +-Dorg.jetbrains.projector.agent.path=PATH_TO_AGENT_JAR \ +-Dorg.jetbrains.projector.agent.classToLaunch=FQN_OF_YOUR_MAIN_CLASS \ +-classpath YOUR_CLASSPATH:PATH_TO_AGENT_JAR \ +org.jetbrains.projector.agent.AgentLauncher \ +YOUR_MAIN_ARGUMENTS ``` +### Modifying your application code +You can launch sharing of your application dynamically from your code, just call `AgentLauncher.attachAgent` method. + ### Run with Gradle tasks -There are two gradle tasks for running app with server: +There are two gradle tasks for running server. They are handy when developing. To enable them, you should set some properties in `local.properties` file in the project root. Use [local.properties.example](../local.properties.example) as a reference. + +1. `runWithAgent` — launch your app with Projector Server. Required properties: + * `projectorLauncher.targetClassPath` — classPath of your application; + * `projectorLauncher.classToLaunch` — your application main class. -1. `runWithAgent` - launch your app with projector server. For enabling this task set properties in `local.properties` file in the project root: - * `projectorLauncher.targetClassPath` - classPath of your application; - * `projectorLauncher.classToLaunch` - your application main class. - -2. `runIdeaWithAgent` - launch IDEA with projector server. For enabling this task set properties in `local.properties` file in the project root: - * `projectorLauncher.ideaPath` - path to IDEA root directory. +2. `runIdeaWithAgent` — launch IntelliJ IDEA with Projector Server. Required property: + * `projectorLauncher.ideaPath` — path to IDEA's root directory. diff --git a/projector-agent/build.gradle b/projector-agent/build.gradle index 0c5c4511..0acee183 100644 --- a/projector-agent/build.gradle +++ b/projector-agent/build.gradle @@ -2,14 +2,15 @@ plugins { id("org.jetbrains.kotlin.jvm") } -def preMainClassName = "org.jetbrains.projector.agent.MainAgent" +def agentClassName = "org.jetbrains.projector.agent.MainAgent" +def launcherClassName = "org.jetbrains.projector.agent.AgentLauncher" -compileKotlin { - kotlinOptions.jvmTarget = targetJvm +kotlin { + explicitApi() } -repositories { - maven { url "https://jitpack.io" } +compileKotlin { + kotlinOptions.jvmTarget = targetJvm } dependencies { @@ -34,7 +35,8 @@ jar { attributes( "Can-Redefine-Classes": true, "Can-Retransform-Classes": true, - "Premain-Class": preMainClassName, + "Agent-Class": agentClassName, + "Main-Class": launcherClassName, ) } @@ -51,11 +53,15 @@ println("------------------------------------------------") if (serverTargetClasspath != null && serverClassToLaunch != null) { task runWithAgent(type: JavaExec) { group = "projector" - main = serverClassToLaunch + main = launcherClassName classpath(sourceSets.main.runtimeClasspath, jar, "$serverTargetClasspath") jvmArgs = [ - "-javaagent:${jar.outputs.files.singleFile}=${jar.outputs.files.singleFile}", + "-Djdk.attach.allowAttachSelf=true", + "-Dswing.bufferPerWindow=false", + "-Dorg.jetbrains.projector.agent.path=${project.file("build/libs/${project.name}-${project.version}.jar")}", + "-Dorg.jetbrains.projector.agent.classToLaunch=$serverClassToLaunch", ] + dependsOn(jar) } } @@ -72,11 +78,13 @@ if (ideaPath != null) { task runIdeaWithAgent(type: JavaExec) { group = "projector" - main = "com.intellij.idea.Main" + main = launcherClassName classpath(sourceSets.main.runtimeClasspath, jar, "$ideaClassPath", "$jdkHome/../lib/tools.jar") jvmArgs = [ - "-javaagent:${jar.outputs.files.singleFile}=${jar.outputs.files.singleFile}", - "-Dorg.jetbrains.projector.server.classToLaunch=com.intellij.idea.Main", + "-Djdk.attach.allowAttachSelf=true", + "-Dswing.bufferPerWindow=false", + "-Dorg.jetbrains.projector.agent.path=${project.file("build/libs/${project.name}-${project.version}.jar")}", + "-Dorg.jetbrains.projector.agent.classToLaunch=com.intellij.idea.Main", "-Didea.paths.selector=ProjectorIntelliJIdea2019.3", "-Didea.jre.check=true", "-Didea.is.internal=true", diff --git a/projector-agent/src/main/kotlin/org/jetbrains/projector/agent/AgentLauncher.kt b/projector-agent/src/main/kotlin/org/jetbrains/projector/agent/AgentLauncher.kt new file mode 100644 index 00000000..c1bde17d --- /dev/null +++ b/projector-agent/src/main/kotlin/org/jetbrains/projector/agent/AgentLauncher.kt @@ -0,0 +1,74 @@ +/* + * GNU General Public License version 2 + * + * Copyright (C) 2019-2020 JetBrains s.r.o. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +package org.jetbrains.projector.agent + +import com.sun.tools.attach.VirtualMachine +import java.lang.management.ManagementFactory +import java.lang.reflect.Method + +public object AgentLauncher { + + private fun checkProperty(name: String, expected: String) { + val actual = System.getProperty(name) + if (actual != expected) { + println("System property `$name` is incorrect: expected <$expected>, got <$actual>") + } + } + + private fun getNotNullProperty(name: String): String { + return requireNotNull(System.getProperty(name)) { "Can't launch: no system property `$name` defined..." } + } + + private fun getMainMethodOf(canonicalClassName: String): Method { + val mainClass = Class.forName(canonicalClassName) + return mainClass.getMethod("main", Array::class.java) + } + + @JvmStatic + public fun attachAgent(agentJar: String) { + println("dynamically attaching agent...") + + checkProperty("swing.bufferPerWindow", false.toString()) + checkProperty("jdk.attach.allowAttachSelf", true.toString()) + + val nameOfRunningVM = ManagementFactory.getRuntimeMXBean().name + val pid = nameOfRunningVM.substringBefore('@') + + try { + val vm = VirtualMachine.attach(pid)!! + vm.loadAgent(agentJar, agentJar) + vm.detach() + } + catch (e: Exception) { + throw RuntimeException(e) + } + + println("dynamically attaching agent... - done") + } + + @JvmStatic + public fun main(args: Array) { + val classToLaunch = getNotNullProperty("org.jetbrains.projector.agent.classToLaunch") + val agentJar = getNotNullProperty("org.jetbrains.projector.agent.path") + + attachAgent(agentJar) + + getMainMethodOf(classToLaunch).invoke(null, args) + } +} diff --git a/projector-agent/src/main/kotlin/org/jetbrains/projector/agent/CommandsHandler.kt b/projector-agent/src/main/kotlin/org/jetbrains/projector/agent/CommandsHandler.kt index 04ef50dd..54904cae 100644 --- a/projector-agent/src/main/kotlin/org/jetbrains/projector/agent/CommandsHandler.kt +++ b/projector-agent/src/main/kotlin/org/jetbrains/projector/agent/CommandsHandler.kt @@ -36,7 +36,7 @@ import java.awt.image.BufferedImage import java.awt.image.BufferedImageOp import javax.swing.UIManager -object CommandsHandler { +internal object CommandsHandler { fun isSupportedCommand(commandName: String) = commandName in commandsMap diff --git a/projector-agent/src/main/kotlin/org/jetbrains/projector/agent/GraphicsInterceptor.kt b/projector-agent/src/main/kotlin/org/jetbrains/projector/agent/GraphicsInterceptor.kt index d5047f43..033d40ee 100644 --- a/projector-agent/src/main/kotlin/org/jetbrains/projector/agent/GraphicsInterceptor.kt +++ b/projector-agent/src/main/kotlin/org/jetbrains/projector/agent/GraphicsInterceptor.kt @@ -25,6 +25,7 @@ import org.jetbrains.projector.awt.peer.PMouseInfoPeer import org.jetbrains.projector.common.protocol.toClient.ServerDrawCommandsEvent import org.jetbrains.projector.common.protocol.toClient.ServerWindowEvent import org.jetbrains.projector.server.ProjectorServer +import org.jetbrains.projector.server.log.Logger import org.jetbrains.projector.server.service.ProjectorDrawEventQueue import org.jetbrains.projector.server.util.unprotect import sun.awt.NullComponentPeer @@ -33,7 +34,7 @@ import java.awt.* import java.awt.peer.ComponentPeer import javax.swing.JComponent -object GraphicsInterceptor { +internal object GraphicsInterceptor { private var commands = mutableListOf() private var paintToOffscreenInProgress = false @@ -93,7 +94,7 @@ object GraphicsInterceptor { @Suppress("unused") @JvmStatic fun endPaintToOffscreen() { - currentQueue!!.commands.add(commands) + currentQueue?.commands?.add(commands) ?: logger.debug { "currentQueue == null" } commands = mutableListOf() paintToOffscreenInProgress = false currentQueue = null @@ -195,4 +196,6 @@ object GraphicsInterceptor { } private operator fun Point.minus(other: Point) = Point(x - other.x, y - other.y) + + private val logger = Logger(GraphicsInterceptor::class.simpleName!!) } diff --git a/projector-agent/src/main/kotlin/org/jetbrains/projector/agent/GraphicsState.kt b/projector-agent/src/main/kotlin/org/jetbrains/projector/agent/GraphicsState.kt index 6c0bade0..a2118535 100644 --- a/projector-agent/src/main/kotlin/org/jetbrains/projector/agent/GraphicsState.kt +++ b/projector-agent/src/main/kotlin/org/jetbrains/projector/agent/GraphicsState.kt @@ -26,7 +26,7 @@ import sun.java2d.SunGraphics2D import java.awt.* import java.awt.geom.AffineTransform -data class GraphicsState( +internal data class GraphicsState( val transform: AffineTransform, val clip: Shape?, val paint: Paint, diff --git a/projector-agent/src/main/kotlin/org/jetbrains/projector/agent/GraphicsTransformer.kt b/projector-agent/src/main/kotlin/org/jetbrains/projector/agent/GraphicsTransformer.kt index 2e552167..4e80c644 100644 --- a/projector-agent/src/main/kotlin/org/jetbrains/projector/agent/GraphicsTransformer.kt +++ b/projector-agent/src/main/kotlin/org/jetbrains/projector/agent/GraphicsTransformer.kt @@ -28,7 +28,7 @@ import java.lang.instrument.IllegalClassFormatException import java.security.ProtectionDomain -class GraphicsTransformer : ClassFileTransformer { +internal class GraphicsTransformer : ClassFileTransformer { @Throws(IllegalClassFormatException::class) override fun transform( loader: ClassLoader?, @@ -83,6 +83,7 @@ class GraphicsTransformer : ClassFileTransformer { ): ByteArray { logger.debug { "Loading SunGraphics2D..." } val clazz = getClassFromClassfileBuffer(classPath, classfileBuffer) + clazz.defrost() clazz.declaredBehaviors.forEach { if (CommandsHandler.isSupportedCommand(it.longName)) { if ((it.methodInfo.accessFlags and AccessFlag.STATIC) > 0) { @@ -106,6 +107,7 @@ class GraphicsTransformer : ClassFileTransformer { ): ByteArray { logger.debug { "Loading SunVolatileImage..." } val clazz = getClassFromClassfileBuffer(classPath, classfileBuffer) + clazz.defrost() val createGraphicsMethod = clazz.getDeclaredMethod("createGraphics") createGraphicsMethod.insertBefore(""" $DRAW_HANDLER_CLASS_LOADING @@ -123,6 +125,7 @@ class GraphicsTransformer : ClassFileTransformer { ): ByteArray { logger.debug { "Loading BufferedImage..." } val clazz = getClassFromClassfileBuffer(classPath, classfileBuffer) + clazz.defrost() val createGraphicsMethod = clazz.getDeclaredMethod("createGraphics") createGraphicsMethod.insertBefore(""" $DRAW_HANDLER_CLASS_LOADING @@ -140,6 +143,7 @@ class GraphicsTransformer : ClassFileTransformer { ): ByteArray { logger.debug { "Loading BalloonImpl..." } val clazz = getClassFromClassfileBuffer(classPath, classfileBuffer) + clazz.defrost() println(clazz) val initImage = clazz.getDeclaredMethod("initComponentImage") initImage.insertBefore(""" @@ -165,6 +169,7 @@ class GraphicsTransformer : ClassFileTransformer { ): ByteArray { logger.debug { "Loading Component..." } val clazz = getClassFromClassfileBuffer(classPath, classfileBuffer) + clazz.defrost() val updateCursorImmediatelyMethod = clazz.getDeclaredMethod("updateCursorImmediately") updateCursorImmediatelyMethod.insertAfter(""" $DRAW_HANDLER_CLASS_LOADING @@ -182,6 +187,7 @@ class GraphicsTransformer : ClassFileTransformer { ): ByteArray { logger.debug { "Loading JComponent..." } val clazz = getClassFromClassfileBuffer(classPath, classfileBuffer) + clazz.defrost() val paintToOffscreenMethod = clazz.getDeclaredMethod("paintToOffscreen") paintToOffscreenMethod.insertBefore(""" $DRAW_HANDLER_CLASS_LOADING diff --git a/projector-agent/src/main/kotlin/org/jetbrains/projector/agent/MainAgent.kt b/projector-agent/src/main/kotlin/org/jetbrains/projector/agent/MainAgent.kt index 424dad33..46d37628 100644 --- a/projector-agent/src/main/kotlin/org/jetbrains/projector/agent/MainAgent.kt +++ b/projector-agent/src/main/kotlin/org/jetbrains/projector/agent/MainAgent.kt @@ -16,29 +16,44 @@ * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ +@file:Suppress("JAVA_MODULE_DOES_NOT_EXPORT_PACKAGE") package org.jetbrains.projector.agent import javassist.ClassPool +import javassist.LoaderClassPath import org.jetbrains.projector.server.log.Logger import java.lang.instrument.Instrumentation -import java.util.jar.JarFile -object MainAgent { +public object MainAgent { + private val logger = Logger(MainAgent::class.simpleName!!) @JvmStatic - fun premain(args: String?, instrumentation: Instrumentation) { - logger.info { "Projector agent working..." } + public fun agentmain(args: String?, instrumentation: Instrumentation) { + logger.info { "agentmain start, args=$args" } + + val threads = Thread.getAllStackTraces().keys + val classLoaders = threads.mapNotNull { it.contextClassLoader }.toSet() + logger.info { "Found classloaders, appending to Javassist: ${classLoaders.joinToString()}" } + classLoaders.forEach { ClassPool.getDefault().appendClassPath(LoaderClassPath(it)) } // Override swing property before swing initialized. Need for MacOS. - System.setProperty("swing.bufferPerWindow", false.toString()) + //System.setProperty("swing.bufferPerWindow", false.toString()) // todo: this doesn't work because Swing is initialized already // Make DrawHandler class visible for System classloader - instrumentation.appendToSystemClassLoaderSearch(JarFile(args)) - - // Make DrawHandler class visible for Javassist - ClassPool.getDefault().insertClassPath(args) + //instrumentation.appendToSystemClassLoaderSearch(JarFile(args)) // todo: seems not needed (maybe after appending all classloaders) instrumentation.addTransformer(GraphicsTransformer(), true) + + instrumentation.retransformClasses( + sun.java2d.SunGraphics2D::class.java, + sun.awt.image.SunVolatileImage::class.java, + java.awt.image.BufferedImage::class.java, + java.awt.Component::class.java, + javax.swing.JComponent::class.java, + //Class.forName("com.intellij.ui.BalloonImpl\$MyComponent"), // todo + ) + + logger.info { "agentmain finish" } } } diff --git a/projector-plugin/README.md b/projector-plugin/README.md index 90032cbe..534c79b8 100644 --- a/projector-plugin/README.md +++ b/projector-plugin/README.md @@ -6,7 +6,7 @@ Please note that it's an experimental technology. If you want simultaneous collaborative editing, please try [Code With Me](https://www.jetbrains.com/help/idea/code-with-me.html) solution. Projector doesn't support that. ## Building -This will give you a zip file with Idea plugin: +This will give you a zip file with IntelliJ plugin: ```shell script ./gradlew :projector-plugin:buildPlugin @@ -15,12 +15,4 @@ This will give you a zip file with Idea plugin: This command creates a zip file in the `projector-plugin/build/distributions` dir. ## Usage -Install the plugin into IDEA via `Install plugin from disk...` menu item in Plugins settings. New menu item `Projector` will appear next to the `Help` in the title bar. - -## Emergency brake -Plugin sets itself as javaagent by modifying the `vmoptions` file. In case of serious problems with javaagent the whole IDEA may not start. To fix it you have to manually remove a line containing `-javaagent` and `projector-plugin` from your `vmoptions` file. - -You can find this file as described here: . Examples: -* Linux: `~/./config` (example `~/.IntelliJIdea2019.3/config`). -* macOS: `~/Library/Preferences/` (example `~/Library/Preferences/IntelliJIdea2019.3`). -* Windows: `%HOMEPATH%\.\config` (example `C:\Users\JohnS\.IntelliJIdea2019.3\config`). +Install the plugin into IntelliJ IDEA via `Install plugin from disk...` menu item in Plugins settings. New menu item `Projector` will appear next to the `Help` in the title bar. diff --git a/projector-plugin/build.gradle.kts b/projector-plugin/build.gradle.kts index 8a3f9777..26c1ade4 100644 --- a/projector-plugin/build.gradle.kts +++ b/projector-plugin/build.gradle.kts @@ -19,7 +19,7 @@ import org.jetbrains.intellij.tasks.PatchPluginXmlTask plugins { - id("org.jetbrains.kotlin.jvm") + kotlin("jvm") id("org.jetbrains.intellij") } @@ -32,6 +32,10 @@ intellij { updateSinceUntilBuild = false } +(tasks["runIde"] as JavaExec).apply { + jvmArgs = jvmArgs.orEmpty() + listOf("-Djdk.attach.allowAttachSelf=true", "-Dswing.bufferPerWindow=false") +} + tasks.withType { changeNotes( """ diff --git a/projector-plugin/src/main/kotlin/ActivateAction.kt b/projector-plugin/src/main/kotlin/ActivateAction.kt new file mode 100644 index 00000000..ee47afd6 --- /dev/null +++ b/projector-plugin/src/main/kotlin/ActivateAction.kt @@ -0,0 +1,33 @@ +/* + * GNU General Public License version 2 + * + * Copyright (C) 2019-2020 JetBrains s.r.o. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.project.DumbAwareAction + +class ActivateAction : DumbAwareAction() { + + override fun actionPerformed(e: AnActionEvent) { + ProjectorService.activate() + } + + override fun update(e: AnActionEvent) { + val state = ProjectorService.enabled == EnabledState.NO_VM_OPTIONS_AND_DISABLED + e.presentation.isEnabled = state + e.presentation.isVisible = state + } +} diff --git a/projector-plugin/src/main/kotlin/CopyUrlAction.kt b/projector-plugin/src/main/kotlin/CopyUrlAction.kt index d7f3729f..d31ce833 100644 --- a/projector-plugin/src/main/kotlin/CopyUrlAction.kt +++ b/projector-plugin/src/main/kotlin/CopyUrlAction.kt @@ -24,6 +24,7 @@ import java.net.Inet4Address import java.net.NetworkInterface class CopyUrlAction : DumbAwareAction() { + override fun actionPerformed(e: AnActionEvent) { val dockerVendor = byteArrayOf(0x02.toByte(), 0x42.toByte()) @@ -55,7 +56,8 @@ class CopyUrlAction : DumbAwareAction() { } override fun update(e: AnActionEvent) { - e.presentation.isEnabled = ProjectorService.instance.enabled - e.presentation.isVisible = ProjectorService.instance.enabled + val state = ProjectorService.enabled == EnabledState.HAS_VM_OPTIONS_AND_ENABLED + e.presentation.isEnabled = state + e.presentation.isVisible = state } } diff --git a/projector-plugin/src/main/kotlin/DisableAction.kt b/projector-plugin/src/main/kotlin/DisableAction.kt index f8002fba..d506bb49 100644 --- a/projector-plugin/src/main/kotlin/DisableAction.kt +++ b/projector-plugin/src/main/kotlin/DisableAction.kt @@ -20,12 +20,14 @@ import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.project.DumbAwareAction class DisableAction : DumbAwareAction() { + override fun actionPerformed(e: AnActionEvent) { - ProjectorService.instance.disable() + ProjectorService.disable() } override fun update(e: AnActionEvent) { - e.presentation.isEnabled = ProjectorService.instance.enabled - e.presentation.isVisible = ProjectorService.instance.enabled + val state = ProjectorService.enabled == EnabledState.HAS_VM_OPTIONS_AND_ENABLED + e.presentation.isEnabled = state + e.presentation.isVisible = state } } diff --git a/projector-plugin/src/main/kotlin/EnableAction.kt b/projector-plugin/src/main/kotlin/EnableAction.kt index 2bf1e39c..bd12ee54 100644 --- a/projector-plugin/src/main/kotlin/EnableAction.kt +++ b/projector-plugin/src/main/kotlin/EnableAction.kt @@ -20,12 +20,14 @@ import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.project.DumbAwareAction class EnableAction : DumbAwareAction() { + override fun actionPerformed(e: AnActionEvent) { - ProjectorService.instance.enable() + ProjectorService.enable() } override fun update(e: AnActionEvent) { - e.presentation.isEnabled = !ProjectorService.instance.enabled - e.presentation.isVisible = !ProjectorService.instance.enabled + val state = ProjectorService.enabled == EnabledState.HAS_VM_OPTIONS_AND_DISABLED + e.presentation.isEnabled = state + e.presentation.isVisible = state } } diff --git a/projector-plugin/src/main/kotlin/ProjectorService.kt b/projector-plugin/src/main/kotlin/ProjectorService.kt index c3ab75ac..1fbe28cb 100644 --- a/projector-plugin/src/main/kotlin/ProjectorService.kt +++ b/projector-plugin/src/main/kotlin/ProjectorService.kt @@ -20,85 +20,73 @@ import com.intellij.diagnostic.VMOptions import com.intellij.ide.plugins.PluginManager import com.intellij.ide.plugins.PluginManagerConfigurable import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.ApplicationNamesInfo import com.intellij.openapi.application.ex.ApplicationEx -import com.intellij.openapi.components.PersistentStateComponent -import com.intellij.openapi.components.ServiceManager -import com.intellij.openapi.components.State -import com.intellij.openapi.components.Storage import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.extensions.PluginId import com.intellij.openapi.ui.Messages import com.intellij.openapi.util.io.FileUtil +import org.jetbrains.projector.agent.AgentLauncher import java.io.File import java.util.function.Function +import javax.swing.JOptionPane +import javax.swing.SwingUtilities -class ProjectorConfig : PersistentStateComponent { - var enabled: Boolean = false - override fun getState(): ProjectorConfig? { - return this - } - - override fun loadState(state: ProjectorConfig) { - enabled = state.enabled - } +enum class EnabledState { + NO_VM_OPTIONS_AND_DISABLED, + HAS_VM_OPTIONS_AND_DISABLED, + HAS_VM_OPTIONS_AND_ENABLED, } -@State(name = "Projector", storages = [Storage("ProjectorConfig.xml")]) -class ProjectorService : PersistentStateComponent { - private var config: ProjectorConfig = ProjectorConfig() +object ProjectorService { + private val logger = Logger.getInstance(ProjectorService::class.java) private val plugin = PluginManager.getPlugin(PluginId.getId("org.jetbrains.projector-plugin"))!! - - private fun vmoptions(): File? { - return try { - VMOptions.getWriteFile()?.also { logger.info("vmoptions: ${it.absolutePath}") } - } - catch (ex: Throwable) { - logger.warn("Failed to read vmoptions: $ex") - null + var enabled: EnabledState = when (areRequiredVmOptionsPresented()) { + true -> EnabledState.HAS_VM_OPTIONS_AND_DISABLED + false -> EnabledState.NO_VM_OPTIONS_AND_DISABLED + } + private set + + fun activate() { + if (confirmRestart( + "Before enabling Projector for the first time, some run arguments (VM properties) should be set. Can I set them and restart the IDE now?")) { + getVMOptions()?.let { (content, writeFile) -> + content + .lineSequence() + .filterNot { it.startsWith("-Dswing.bufferPerWindow") || it.startsWith("-Djdk.attach.allowAttachSelf") } + .plus("-Dswing.bufferPerWindow=false") + .plus("-Djdk.attach.allowAttachSelf=true") + .joinToString(separator = System.lineSeparator()) + .let { FileUtil.writeToFile(writeFile, it) } + + restartIde() + } ?: SwingUtilities.invokeLater { + JOptionPane.showMessageDialog( + null, + "Can't change VM options. Please see logs to understand the error", + "Can't set up...", + JOptionPane.ERROR_MESSAGE, + ) + } } } - val enabled: Boolean - get() = config.enabled - - fun disable() { - if (!confirmRestart()) return - - getVMOptions()?.let { (content, writeFile) -> - content - .lineSequence() - .filterNot { it.startsWith("-javaagent:") && it.contains("projector-plugin") } - .joinToString(separator = System.lineSeparator()) - .let { FileUtil.writeToFile(writeFile, it) } - - config.enabled = false - exit() + if (confirmRestart("To disable Projector, restart is needed. Can I restart the IDE now?")) { + restartIde() } } fun enable() { - if (!confirmRestart()) return + attachDynamicAgent() + enabled = EnabledState.HAS_VM_OPTIONS_AND_ENABLED + } - val agentJar = "${plugin.path}/lib/projector-agent-${plugin.version}.jar" - val agentOption = "-javaagent:$agentJar=$agentJar" - logger.warn("agentOption: $agentOption") - - getVMOptions()?.let { (content, writeFile) -> - content - .lineSequence() - .filterNot { it.startsWith("-javaagent:") && it.contains("projector-plugin") } - .plus(agentOption) - .joinToString(separator = System.lineSeparator()) - .let { FileUtil.writeToFile(writeFile, it) } - - config.enabled = true - exit() - } + private fun areRequiredVmOptionsPresented(): Boolean { + return System.getProperty("swing.bufferPerWindow")?.toBoolean() == false && + System.getProperty("jdk.attach.allowAttachSelf")?.toBoolean() == true } private fun getVMOptions(): Pair? { @@ -124,27 +112,18 @@ class ProjectorService : PersistentStateComponent { return Pair(s, writeFile) } - private fun confirmRestart() : Boolean { - val title = "Restart to ${if (enabled) "disable" else "enable"} Projector" - val message = Function { action: String? -> - "$action ${ApplicationNamesInfo.getInstance().fullProductName} to ${if (enabled) "remove" else "add"} java agent?"} + private fun confirmRestart(messageString: String): Boolean { + val title = "Restart is needed..." + val message = Function { messageString } return PluginManagerConfigurable.showRestartDialog(title, message) == Messages.YES } - - private fun exit() { + private fun restartIde() { (ApplicationManager.getApplication() as ApplicationEx).restart(true) } - companion object { - val instance: ProjectorService by lazy { ServiceManager.getService(ProjectorService::class.java)!! } - } - - override fun getState(): ProjectorConfig? { - return config - } - - override fun loadState(state: ProjectorConfig) { - config = state + private fun attachDynamicAgent() { + val agentJar = "${plugin.path}/lib/projector-agent-${plugin.version}.jar" + AgentLauncher.attachAgent(agentJar) } } diff --git a/projector-plugin/src/main/kotlin/RegisterPluginInstallerStateListener.kt b/projector-plugin/src/main/kotlin/RegisterPluginInstallerStateListener.kt index be81955a..bfe4701e 100644 --- a/projector-plugin/src/main/kotlin/RegisterPluginInstallerStateListener.kt +++ b/projector-plugin/src/main/kotlin/RegisterPluginInstallerStateListener.kt @@ -28,8 +28,8 @@ class RegisterPluginInstallerStateListener : StartupActivity { override fun install(descriptor: IdeaPluginDescriptor) {} override fun uninstall(descriptor: IdeaPluginDescriptor) { - if (ProjectorService.instance.enabled) { - ProjectorService.instance.disable() + if (ProjectorService.enabled == EnabledState.HAS_VM_OPTIONS_AND_ENABLED) { + ProjectorService.disable() } } }) diff --git a/projector-plugin/src/main/resources/META-INF/plugin.xml b/projector-plugin/src/main/resources/META-INF/plugin.xml index 2cb74742..4faf754a 100644 --- a/projector-plugin/src/main/resources/META-INF/plugin.xml +++ b/projector-plugin/src/main/resources/META-INF/plugin.xml @@ -30,7 +30,6 @@ com.intellij.modules.platform - @@ -40,6 +39,8 @@ + diff --git a/projector-server/src/main/kotlin/org/jetbrains/projector/server/ProjectorServer.kt b/projector-server/src/main/kotlin/org/jetbrains/projector/server/ProjectorServer.kt index bedd4a56..0e894230 100644 --- a/projector-server/src/main/kotlin/org/jetbrains/projector/server/ProjectorServer.kt +++ b/projector-server/src/main/kotlin/org/jetbrains/projector/server/ProjectorServer.kt @@ -917,14 +917,20 @@ class ProjectorServer private constructor( SwingUtilities.invokeLater { mainWindows.forEach { val point = AwtPoint(PGraphicsDevice.clientShift) + var widthWithInsets = width + var heightWithInsets = height if (it is Frame) { it.insets?.let { i -> point.x -= i.left point.y -= i.top + + // since main windows have no borders on the client now, we should move insets out of client's viewport: + widthWithInsets += i.left + i.right + heightWithInsets += i.top + i.bottom } } - it.setBounds(point.x, point.y, width, height) + it.setBounds(point.x, point.y, widthWithInsets, heightWithInsets) it.revalidate() } } @@ -936,8 +942,9 @@ class ProjectorServer private constructor( ProjectorAwtInitializer.initProjectorAwt() if (isAgent) { - setupAgentSystemProperties() - setupAgentSingletons() + // todo: make it work with dynamic agent + //setupAgentSystemProperties() + //setupAgentSingletons() } else { setupSystemProperties() diff --git a/projector-server/src/main/kotlin/org/jetbrains/projector/server/util/FontCacher.kt b/projector-server/src/main/kotlin/org/jetbrains/projector/server/util/FontCacher.kt index 5e0ce364..266ebb9d 100644 --- a/projector-server/src/main/kotlin/org/jetbrains/projector/server/util/FontCacher.kt +++ b/projector-server/src/main/kotlin/org/jetbrains/projector/server/util/FontCacher.kt @@ -22,6 +22,7 @@ package org.jetbrains.projector.server.util import org.jetbrains.projector.common.protocol.data.FontDataHolder import org.jetbrains.projector.common.protocol.data.TtfFontData +import org.jetbrains.projector.server.service.ProjectorFontProvider import sun.font.CompositeFont import sun.font.FileFont import sun.font.Font2D @@ -74,9 +75,6 @@ object FontCacher { } private fun Font.getFilePath(): String? { - // don't use the font2DHandle field here because it is initialized in the getFont2D() method - val font2D = getFont2DMethod.invoke(this) as Font2D - - return font2D.getFilePath() + return ProjectorFontProvider.findFont2D(this.name, this.style, 0).getFilePath() } }