20230429 IaC之IDE,也许是下一个ChatIDE?

0.背景

继基于插件做到代码部分语义可视化、以及反向控制IDE浏览代码跳转之后,我想尝试进一步获得IDE的控制权,来完成其他想法中的数据收集和增强自动化办公的程度,小小的推进一下IaC的进程,顺带着给AI自动化控制IDE开启一个新的道路,给AI“看”和“动”的基础能力。

这个想法启发于黄老师在Archguard中使用的createRepl方法,可以在自己的应用程序中内嵌一个jupyter的后端来完成自定义DSL的REPL(参考 https://github.com/archguard/archguard/blob/master/architecture-as-code/repl-api/src/main/kotlin/org/archguard/aaac/repl/compiler/KotlinReplWrapper.kt )。要是模仿他的方法在插件中启动一个embedded-jupyter-repl后端,能否通过repl来越过jupyter来执行插件中的方法呢?

1.需求分析

实际上IDEA本身也是一个java进程,有着自己的JVM和类加载器,其实相同的功能还在官方的IDE Scripting Console中有所体现、还有名为liveplugin的IDEAplugin,都可以做到动态在已经运行了的IDE的JVM进程中执行额外的代码

众所周知Java的运行是需要先编译成字节码再加载到JVM中的,那么也就意味着如果没有字节码增强和动态类加载技术,仅凭借正常的Java反射是很难做到的。理论上大部分程序在启动之后其行为就已经固化,不太可能轻易的通过外部进行干预。因此jupyter-repl在正常情况下是没法控制它的父进程的才对。但createRepl方法中的embed参数又让我起了怀疑,特此继续调研之前设想的IDE中REPL的可行性。

实现这个功能的思路上大致可以分为暴力穷举方案、RPC方案、字节码增强&动态类加载方案、移花接木方案

暴力穷举:人工或通过某种形式提取IDEA目前已知所有公开的API,生成delegate代码写入一个插件中,通过RPC等形式暴露给外部使用,这个方法难度最低但工作量最大,并且随着IDEA自身的升级需要持续维护

RPC方案:相比暴力穷举做一层抽象,使用反射进行动态调用再暴露结果给外部,这个方案难度比前一个高,工作量少了很多。潜在的坑是数据类型不太好处理,容易出运行时的问题

字节码增强&动态类加载方案:使用asm等字节码增强技术实时修改JVM运行时的字节码来达到,这个方案难度继续提高,工作量相比前一个差不多。潜在的坑是字节码增强技术配合任意代码使用,不异于重写一个编译器,可能会重新发明一次JShell和部分的javac。

因为我这么懒必然不会优先考虑前面的,因此一直在关注有什么已经做的差不多的功能,稍加拼接就能复用的方案。

移花接木方案1:因为按之前的思路是想继续使用Java的生态直接复用,特意看了下java在9之后出的JShell,理论上这个能作为一个后端使用,让这个后端和已有的JVM共享类加载器,麻烦的是需要自己解决类加载器的问题

移花接木方案2:IDE自带的部分功能和历史上有人实现的插件做到了类似的能力,参考他们的原理和代码稍加修改也许能和jupyter结合起来。

2.前端方案

jupyter、shell //any way,只要方便交互就好,按历史经验和套路,只要能用shell或文本交互的最后都比较容易再做二次开发
https://github.com/jupyterlab/jupyter-ai
https://github.com/Kotlin/kotlin-jupyter
https://github.com/dev-assistant/skykoma-plugin-idea/blob/bundle-scripting-console/src/main/kotlin/cn/hylstudio/skykoma/plugin/idea/KotlinJsr223JvmScriptEngine4Idea.kt
https://github.com/unit-mesh/auto-dev

3.执行引擎方案

3.1.embedded kotlin compiler //demo测试成功

实际的依赖是kotlin-scripting-compiler-embeddable

参考IDE Scripting Console的实现方式,兼容JSR-223的脚本引擎都可以调用IDE自身公开的接口,通过追踪IDEA源码可以看到入口,参考附录。

可以从源码看到IDEA主平台只声明了接口,由各自的插件自行实现Factory接口,根据追踪可以看到名为KotlinJsr223StandardScriptEngineFactory4Idea的类,这里已经处理好了IDEA主JVM的类加载器,因此模仿IDEA的核心代码调用即可。

官方的功能相对比较简单且不是核心功能,只能选取部分代码传给script engine执行动作。因和不同的实现相关,控制台并不都会显示输出。因此相关的资料也及其稀少,通过阅读大量源码和官方的bug反馈,对比git log、release note等方式才能找到一点有用的信息作为参考,另外还需要防备官方后续废弃这个功能。

注意:在某些版本上无法使用这个功能,需要开启internal actions菜单才能看到入口,但我打算后续拆出来放插件里通过别的形式触发。
PS: 冷门知识还是得自己动手啊,chatgpt一直在胡编一些不存在的路径,指望AI偷懒只会浪费时间。这部分代码开始在主仓库后来分到了kotlin的仓库,然后又从kotlin拆走回到主仓库。
PPS: 在没有编译环境的情况下,追这么大型的源代码还是需要一些技巧的,这个等后面有闲心再写

测试脚本

环境测试

3.2.kotlin-repl //待详细调研

从kotlin-repl的启动命令可以分析原理,理论上也需要解决类加载器的问题

3.3.jshell

jshell跨越宿主jvm执行动态字节码理论上不可行,推迟

3.4.jupyter 集成 //成功

REPL除了动态执行代码以外,还要包括上下文记录、UI渲染等功能。上下文记录也就是前面定义的变量后面可以继续引用。根据之前测试JSR-223的Java默认实现来看效果不是很好,根本做不到历史的记录。
根据script engine成功的现象debug可以发现,是kotlin的插件自己实现了历史记录的记录,利用这个也许可以和jupyter做对接
那么jupyter-kotlin 的api能否复用呢?理论上可行,尝试安装jupyterlab如下

记录下当前的依赖版本

 

安装完jupyterlab和kotlin-kernel后可以在python包的路径中看到相关的文件夹,kernel的描述文件主要入口是kernel.json,内容如下

其中核心内容是argv,可以看到当kernel启动时候实际执行的是run_kotlin_kernel,其中connection_file每次动态生成,内容如下

文件中描述了本次内核启动需要的端口、标识、协议等信息传递给python脚本。python脚本主要用于拼接启动命令

主要是获取环境变量、寻找java可执行文件路径、获取当前文件夹等信息,拼接的命令如下。

修改run_kernel.py如下

目前add_kernel支持设置环境变量之类的参数,理论上可以把这个过程从原流程中剥离出来独立出一个jar来执行,这样可以不破坏原有的python文件同时完成自定义kernel的接入,目前仅是demo直接暴力修改了python脚本不是很优雅

0.0.9初步实现了用自动注册kernel,可手动指定python的目录,支持动态修改kernel.json。

剥离对官方run_kernel.py的依赖,暂时使用shell替换整个文件

20250201更新:更换为独立模块摆脱对官方的依赖 https://github.com/956237586/run_kotlin_kernel_idea

尝试手动指定、远程分配监听端口:根据调用栈可追踪,需要修改逻辑

https://github.com/jupyter-server/jupyter_server

https://github.com/jupyter-server/jupyter_client

fork官方库后修改对应的代码强制使用环境变量中的端口,打tag v8.4.3后可从github安装指定版本

之后可支持在kernel.json的环境变量指定端口

继续分析脚本逻辑,为了方便理解简化启动命令为  java -jar xxx.jar -classpath=xxx xxx.json -home=xxx -debugPort=xxx,可以看到入口是kotlin-jupyter-kernel-0.11.0-385.jar,这个jar是kotlin编写的

根据日志和源码的情况,如果我们想模拟这个过程,只需要让server在idea插件的上下文启动。

因为端口描述的文件每次都是动态生成的,因此修改run_kernel.py把命令行参数传给idea插件,idea插件再启动replserver

在idea插件里启动一个httpserver监听请求,接读取原来的参数调用embedKernel,同时替换classpath为当前线程上下文的classpath,示例请求如下

这样启动之后就可以尝试jupyter中执行kotlin了,遇到的错误如下

避开之前测试引入的库影响同时保证编译通过,仅保留相关的依赖

再次继续测试错误如下

按独立demo调整后错误消失,但我忘了为啥了,下一个错误是构造器初始化失败提示类型不匹配,还去github开了issue问

通过反复和demo对比,发现classLoader的继承关系不一样,虽然是相同的类但被不通的类加载器加载后实际是无法通用的

构造测试脚本

最后根据调整ide scripting console时候的经验,修改最初启动server入口独立Thread的classLoader为idea加载插件所用的classLoader即可解决问题

效果如下

在java11和下面的环境组合下测试,代码分支https://github.com/dev-assistant/skykoma-plugin-idea/tree/test-jupyter-java11

Unable to instantiate class Line_4_jupyter

目前在java17和下面环境组合下测试成功,代码分支https://github.com/dev-assistant/skykoma-plugin-idea/tree/test-jupyter-java17

 

3.5.livePlugin的实现方案//待调研

TODO

附录

JSR-223

https://www.jcp.org/en/jsr/detail?id=223

livePlugin

https://plugins.jetbrains.com/plugin/7282-liveplugin

IDE Scripting Console 功能文档
https://www.jetbrains.com/help/idea/ide-scripting-console.html
IDE Scripting Console 源码
https://github.com/JetBrains/intellij-community/blob/fc580582f211e5f8866462d8c596aaf8e36760ac/platform/lang-impl/src/com/intellij/ide/script/RunIdeConsoleAction.java#L88
内置的Kotlin脚本引擎 源码
https://github.com/JetBrains/intellij-community/blob/master/plugins/kotlin/repl/src/org/jetbrains/kotlin/jsr223/KotlinJsr223StandardScriptEngineFactory4Idea.kt
官方issues
https://youtrack.jetbrains.com/issues?q=%22IDE%20Scripting%20Console%22%20project:%20%7BIntelliJ%20IDEA%7D
https://visualstudio.microsoft.com/zh-hans/visual-cpp-build-tools/
https://mirrors.tuna.tsinghua.edu.cn/help/pypi/

https://saturncloud.io/blog/how-to-get-autocomplete-in-jupyter-notebook-without-using-tab/

https://browse.arxiv.org/pdf/2309.12499.pdf

0 Comments
Leave a Reply