Skip to content
This repository has been archived by the owner on Mar 26, 2024. It is now read-only.

Commit

Permalink
PRJ-129 Attach agent dynamically
Browse files Browse the repository at this point in the history
  • Loading branch information
SerVB committed Oct 20, 2020
1 parent 463c213 commit ad72df9
Show file tree
Hide file tree
Showing 19 changed files with 272 additions and 140 deletions.
34 changes: 20 additions & 14 deletions projector-agent/README.md
Original file line number Diff line number Diff line change
@@ -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.
30 changes: 19 additions & 11 deletions projector-agent/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -34,7 +35,8 @@ jar {
attributes(
"Can-Redefine-Classes": true,
"Can-Retransform-Classes": true,
"Premain-Class": preMainClassName,
"Agent-Class": agentClassName,
"Main-Class": launcherClassName,
)
}

Expand All @@ -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)
}
}

Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>::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<String>) {
val classToLaunch = getNotNullProperty("org.jetbrains.projector.agent.classToLaunch")
val agentJar = getNotNullProperty("org.jetbrains.projector.agent.path")

attachAgent(agentJar)

getMainMethodOf(classToLaunch).invoke(null, args)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<ServerWindowEvent>()

private var paintToOffscreenInProgress = false
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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!!)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?,
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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("""
Expand All @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
}
}
12 changes: 2 additions & 10 deletions projector-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: <https://www.jetbrains.com/help/idea/tuning-the-ide.html#config-directory>. Examples:
* Linux: `~/.<product><version>/config` (example `~/.IntelliJIdea2019.3/config`).
* macOS: `~/Library/Preferences/<product><version>` (example `~/Library/Preferences/IntelliJIdea2019.3`).
* Windows: `%HOMEPATH%\.<product><version>\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.
6 changes: 5 additions & 1 deletion projector-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import org.jetbrains.intellij.tasks.PatchPluginXmlTask

plugins {
id("org.jetbrains.kotlin.jvm")
kotlin("jvm")
id("org.jetbrains.intellij")
}

Expand All @@ -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<PatchPluginXmlTask> {
changeNotes(
"""
Expand Down
Loading

0 comments on commit ad72df9

Please sign in to comment.