├── .github └── workflows │ └── main.yml ├── .gitignore ├── .gitpod.yml ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── LICENSE ├── README.md ├── README_CN.md ├── jbs-client ├── README.md ├── go.mod ├── go.sum ├── main.go ├── request │ └── ws.go └── ui │ ├── constants.go │ ├── input_container.go │ └── log_container.go ├── mvnw ├── mvnw.cmd ├── pom.xml ├── src ├── main │ ├── java │ │ └── w │ │ │ ├── App.java │ │ │ ├── Attach.java │ │ │ ├── Global.java │ │ │ ├── core │ │ │ ├── ExecBundle.java │ │ │ ├── GroovyBundle.java │ │ │ ├── Swapper.java │ │ │ ├── asm │ │ │ │ ├── SbNode.java │ │ │ │ ├── Tool.java │ │ │ │ └── WAdviceAdapter.java │ │ │ ├── compiler │ │ │ │ └── WCompiler.java │ │ │ ├── constant │ │ │ │ └── Codes.java │ │ │ └── model │ │ │ │ ├── BaseClassTransformer.java │ │ │ │ ├── ChangeBodyTransformer.java │ │ │ │ ├── ChangeResultTransformer.java │ │ │ │ ├── DecompileTransformer.java │ │ │ │ ├── OuterWatchTransformer.java │ │ │ │ ├── ReplaceClassTransformer.java │ │ │ │ ├── TraceTransformer.java │ │ │ │ └── WatchTransformer.java │ │ │ ├── util │ │ │ ├── JarInJarClassLoader.java │ │ │ ├── NativeUtils.java │ │ │ ├── RequestUtils.java │ │ │ ├── SpringUtils.java │ │ │ └── WClassLoader.java │ │ │ └── web │ │ │ ├── Httpd.java │ │ │ ├── Websocketd.java │ │ │ └── message │ │ │ ├── ChangeBodyMessage.java │ │ │ ├── ChangeResultMessage.java │ │ │ ├── DecompileMessage.java │ │ │ ├── DeleteMessage.java │ │ │ ├── EvalMessage.java │ │ │ ├── ExecMessage.java │ │ │ ├── LogMessage.java │ │ │ ├── Message.java │ │ │ ├── MessageType.java │ │ │ ├── OuterWatchMessage.java │ │ │ ├── PingMessage.java │ │ │ ├── PongMessage.java │ │ │ ├── ReplaceClassMessage.java │ │ │ ├── RequestMessage.java │ │ │ ├── ResetMessage.java │ │ │ ├── ResponseMessage.java │ │ │ ├── TraceMessage.java │ │ │ └── WatchMessage.java │ └── resources │ │ ├── InlineWrapper.java │ │ ├── META-INF │ │ ├── MANIFEST.MF │ │ └── services │ │ │ └── wshade.com.fasterxml.jackson.databind.Module │ │ ├── nanohttpd │ │ └── index.html │ │ ├── w_Global.c │ │ ├── w_Global.h │ │ ├── w_aarch64.dylib │ │ ├── w_amd64.dll │ │ ├── w_amd64.dylib │ │ └── w_amd64.so └── test │ └── java │ └── w │ └── core │ ├── AbstractService.java │ ├── ChangeBodyTest.java │ ├── ChangeResultTest.java │ ├── ChangeTarget.java │ ├── DecompileTest.java │ ├── ExecuteTest.java │ ├── MyInterface.java │ ├── OuterWatchTest.java │ ├── R.java │ ├── R2.java │ ├── SwapperTest.java │ ├── Target.java │ ├── TestClass.java │ ├── TraceTest.java │ ├── WatchTarget.java │ └── WatchTest.java └── sw-ico.png /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - 'v*' 9 | 10 | jobs: 11 | build-java: 12 | name: Build Java 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up JDK 8 17 | uses: actions/setup-java@v3 18 | with: 19 | java-version: '8' 20 | distribution: 'adopt' 21 | - name: Build Java with Maven 22 | run: | 23 | mvn package && cp target/swapper-0.0.1-SNAPSHOT.jar swapper.jar 24 | shell: bash 25 | - name: Upload Jar 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: artifacts 29 | path: swapper.jar 30 | build-go: 31 | name: Build Go 32 | runs-on: ubuntu-latest 33 | strategy: 34 | matrix: 35 | goos: [windows, darwin, linux] 36 | goarch: [arm64, amd64] 37 | steps: 38 | - uses: actions/checkout@v3 39 | - name: Set up Go 40 | uses: actions/setup-go@v3 41 | with: 42 | go-version: '^1.22' 43 | - name: Build Go Binary 44 | working-directory: ./jbs-client 45 | run: | 46 | EXT="" 47 | if [ "${{ matrix.goos }}" == "windows" ]; then 48 | EXT=".exe" 49 | fi 50 | CGO_ENABLED=0 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -v -o jbs-client-${{ matrix.goos }}-${{ matrix.goarch }}$EXT 51 | shell: bash 52 | - name: Archive Production Artifacts 53 | run: | 54 | zip -r jbs-client-${{ matrix.goos }}-${{ matrix.goarch }}.zip jbs-client/jbs-client-${{ matrix.goos }}-${{ matrix.goarch }}* 55 | shell: bash 56 | - name: Upload Artifacts 57 | uses: actions/upload-artifact@v4 58 | with: 59 | name: artifacts-${{ matrix.goos }}-${{ matrix.goarch }} 60 | path: jbs-client-${{ matrix.goos }}-${{ matrix.goarch }}.zip 61 | # test-download: 62 | # needs: ["build-java", "build-go"] 63 | # runs-on: ubuntu-latest 64 | # steps: 65 | # - name: download 66 | # uses: actions/download-artifact@v4 67 | # - name: tree 68 | # run: | 69 | # tree . 70 | # mv artifacts-*/* artifacts 71 | # tree artifacts 72 | 73 | 74 | release: 75 | if: startsWith(github.ref, 'refs/tags/') 76 | needs: ["build-java", "build-go"] 77 | runs-on: ubuntu-latest 78 | steps: 79 | - name: Download Artifacts 80 | uses: actions/download-artifact@v4 81 | - name: tree 82 | run: | 83 | mv artifacts-*/* artifacts 84 | tree artifacts 85 | - name: Create Release 86 | id: create_release 87 | uses: actions/create-release@v1 88 | env: 89 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 90 | with: 91 | tag_name: ${{ github.ref }} 92 | release_name: Release ${{ github.ref }} 93 | draft: false 94 | prerelease: false 95 | - name: Upload Release Assets 96 | uses: softprops/action-gh-release@v1 97 | with: 98 | files: artifacts/* 99 | env: 100 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 101 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | HELP.md 3 | target/ 4 | !.mvn/wrapper/maven-wrapper.jar 5 | !**/src/main/**/target/ 6 | !**/src/test/**/target/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | 23 | ### NetBeans ### 24 | /nbproject/private/ 25 | /nbbuild/ 26 | /dist/ 27 | /nbdist/ 28 | /.nb-gradle/ 29 | build/ 30 | !**/src/main/**/build/ 31 | !**/src/test/**/build/ 32 | 33 | ### VS Code ### 34 | .vscode/ 35 | dependency-reduced-pom.xml -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # This configuration file was automatically generated by Gitpod. 2 | # Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml) 3 | # and commit this file to your remote git repository to share the goodness with others. 4 | 5 | # Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart 6 | 7 | tasks: 8 | - init: ./mvnw install -DskipTests=false 9 | 10 | 11 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunwu51/JVMByteSwapTool/f7284208d845de5b20465391665b8fc3cbc8781d/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Frank 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JVM ByteSwap Tool 2 | ![logo](sw-ico.png) 3 | 4 | ![actions](https://github.com/sunwu51/JVMByteSwapTool/actions/workflows/main.yml/badge.svg) 5 | 6 | A tool that can hot swap the class byte code while jvm is running. Very suitable for `SpringBoot framework`. 7 | 8 | Based on the jvm instrumentation tech, ASM, javassist and JVMTI. 9 | 10 | # Usage 11 | Download the zip file from the [release](https://github.com/sunwu51/JVMByteSwapTool/releases) page. 12 | 13 | Make sure you have a JDK >= 1.8. 14 | ```bash 15 | $ java -jar swapper.jar 16 | 17 | // All of the java processes will be listed in following 18 | // Choose the pid you want to attach 19 | // Then a web ui will be served at http://localhost:8000 20 | ``` 21 | 22 | Visit this url `http://localhost:8000` then you will get the following Web UI. 23 | 24 | If you want to change the http server port or web socket port: 25 | ```bash 26 | $ java -jar -Dw_http_port=9999 -Dw_ws_port_19999 swapper.jar 27 | ``` 28 | 29 | ![image](https://i.imgur.com/WSKkrxX.png) 30 | 31 | Now you can enjoy the functionalities of swapper tool. 32 | 33 | For example, `Watch` some methods. Trigger this method, and then the params and return value and execution time cost will be printed. 34 | 35 | ![image](https://i.imgur.com/RaEZ1w5.png) 36 | 37 | It's `Watch` one of the functions provided by swapper tool. 38 | 39 | Get more functions and details from the [wiki](https://github.com/sunwu51/JVMByteSwapTool/wiki). 40 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # JVM ByteSwap Tool 2 | ![logo](sw-ico.png) 3 | 4 | ![actions](https://github.com/sunwu51/JVMByteSwapTool/actions/workflows/main.yml/badge.svg) 5 | 6 | 这是一个能在jvm运行时热替换类的字节码的工具,特别适合`Spring Boot`框架,它基于`instrumentation` `ASM` `javassist` `JVMTI`等技术。 7 | 8 | # 用法 9 | 从[release](https://github.com/sunwu51/JVMByteSwapTool/releases)下载jar包,并确保运行环境是`jdk8+` 10 | ```bash 11 | $ java -jar swapper.jar 12 | 13 | // 所有的java进程会被列出 14 | // 选择你要attach的jvm进程 15 | // 然后一个webui就会提供在 http://localhost:8000 16 | ``` 17 | 18 | 打开`http://localhost:8000`你会看到下面的页面,当然如果你想更改端口,可以通过下面启动指令: 19 | ```bash 20 | $ java -jar -Dw_http_port=9999 -Dw_ws_port_19999 swapper.jar 21 | ``` 22 | 23 | ![image](https://i.imgur.com/WSKkrxX.png) 24 | 25 | 现在你就可以体验`swapper`提供的各种功能了,例如`watch`某个方法,触发这个方法的时候,入参返回值和耗时将会被打印出来。 26 | 27 | ![image](https://i.imgur.com/RaEZ1w5.png) 28 | 29 | 这是众多功能之一的`watch`,想要查看更多信息可以查看[wiki](https://github.com/sunwu51/JVMByteSwapTool/wiki). 30 | -------------------------------------------------------------------------------- /jbs-client/README.md: -------------------------------------------------------------------------------- 1 | # A tui for JVMByteSwapperTool 2 | Linux/MacOS supported 3 | 4 | Use the source code 5 | ```bash 6 | $ go mod tidy 7 | $ go run . [options] 8 | ``` 9 | Or use the binary files in the release package (Only linux binary is provided, for other platforms, built by yourself) 10 | ```bash 11 | $ ./jbs-client [options] 12 | ``` 13 | ## options 14 | ``` 15 | --host localhost 16 | --http_port 8000 17 | --ws_port 18000 18 | ``` -------------------------------------------------------------------------------- /jbs-client/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sunwu51/jbs/client 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/charmbracelet/bubbles v0.18.0 7 | github.com/charmbracelet/bubbletea v0.25.0 8 | github.com/charmbracelet/lipgloss v0.9.1 9 | github.com/gorilla/websocket v1.5.1 10 | github.com/thoas/go-funk v0.9.3 11 | ) 12 | 13 | require ( 14 | github.com/atotto/clipboard v0.1.4 // indirect 15 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 16 | github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect 17 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 18 | github.com/mattn/go-isatty v0.0.18 // indirect 19 | github.com/mattn/go-localereader v0.0.1 // indirect 20 | github.com/mattn/go-runewidth v0.0.15 // indirect 21 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect 22 | github.com/muesli/cancelreader v0.2.2 // indirect 23 | github.com/muesli/reflow v0.3.0 // indirect 24 | github.com/muesli/termenv v0.15.2 // indirect 25 | github.com/rivo/uniseg v0.4.6 // indirect 26 | github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect 27 | golang.org/x/net v0.17.0 // indirect 28 | golang.org/x/sync v0.1.0 // indirect 29 | golang.org/x/sys v0.13.0 // indirect 30 | golang.org/x/term v0.13.0 // indirect 31 | golang.org/x/text v0.13.0 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /jbs-client/go.sum: -------------------------------------------------------------------------------- 1 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= 2 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 3 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 4 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 5 | github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= 6 | github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= 7 | github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= 8 | github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= 9 | github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= 10 | github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= 11 | github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= 12 | github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= 13 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 14 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= 16 | github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 17 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 18 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 19 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 20 | github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= 21 | github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 22 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 23 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 24 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 25 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 26 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 27 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= 28 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= 29 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 30 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 31 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 32 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 33 | github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= 34 | github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 35 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 36 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 37 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 38 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 39 | github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg= 40 | github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 41 | github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y= 42 | github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= 43 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 44 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 45 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 46 | github.com/thoas/go-funk v0.9.3 h1:7+nAEx3kn5ZJcnDm2Bh23N2yOtweO14bi//dvRtgLpw= 47 | github.com/thoas/go-funk v0.9.3/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= 48 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 49 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 50 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 51 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 52 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 53 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 54 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 55 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 56 | golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= 57 | golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= 58 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= 59 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 60 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 61 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 62 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 63 | -------------------------------------------------------------------------------- /jbs-client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | tea "github.com/charmbracelet/bubbletea" 8 | "github.com/charmbracelet/lipgloss" 9 | "github.com/sunwu51/jbs/client/request" 10 | "github.com/sunwu51/jbs/client/ui" 11 | ) 12 | 13 | type Model struct { 14 | state int 15 | width int 16 | height int 17 | inputContainer ui.InputContainer 18 | logContainer ui.LogContainer 19 | } 20 | 21 | func (m Model) Init() tea.Cmd { 22 | return nil 23 | } 24 | 25 | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 26 | switch x := msg.(type) { 27 | case tea.KeyMsg: 28 | if x.Type == tea.KeyCtrlC { 29 | return m, tea.Quit 30 | } 31 | case tea.WindowSizeMsg: 32 | m.width, m.height = x.Width, x.Height 33 | ready := 1 34 | m.state = ready 35 | } 36 | i, cmd1 := m.inputContainer.Update(msg) 37 | l, cmd2 := m.logContainer.Update(msg) 38 | m.inputContainer = i 39 | m.logContainer = l 40 | return m, tea.Batch(cmd1, cmd2) 41 | } 42 | 43 | func (m Model) View() string { 44 | if m.width < 100 || m.height < 30 { 45 | return fmt.Sprintf("Window need to larger than 100x30, current=%dx%d", m.width, m.height) 46 | } 47 | initializing := 0 48 | if m.state == initializing { 49 | return "Initializing..." 50 | } 51 | iv := lipgloss.NewStyle().Width(m.width / 2).Render( 52 | m.inputContainer.View()) 53 | lv := m.logContainer.View() 54 | return lipgloss.JoinHorizontal(lipgloss.Bottom, iv, lv) 55 | } 56 | 57 | func main() { 58 | h := flag.String("host", "localhost", "server host") 59 | port1 := flag.Int("http_port", 8000, "http port") 60 | port2 := flag.Int("ws_port", 18000, "ws port") 61 | flag.Parse() 62 | request.Host = *h 63 | request.HttpPort = *port1 64 | request.WsPort = *port2 65 | p := tea.NewProgram(Model{ 66 | inputContainer: ui.NewInputContainer(), 67 | logContainer: ui.NewLogContainer(), 68 | }) 69 | updateMsgChan := make(chan request.AppendLogMsg) 70 | go request.ConnectWebSocket(updateMsgChan) 71 | go func() { 72 | for msg := range updateMsgChan { 73 | p.Send(msg) 74 | } 75 | }() 76 | p.Run() 77 | } 78 | -------------------------------------------------------------------------------- /jbs-client/request/ws.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | "github.com/gorilla/websocket" 10 | ) 11 | 12 | type AppendLogMsg string 13 | 14 | var ws *websocket.Conn 15 | var Host string 16 | var WsPort int 17 | var HttpPort int 18 | 19 | func ConnectWebSocket(updateMsgChan chan<- AppendLogMsg) { 20 | dial := websocket.Dialer{} 21 | c, _, err := dial.Dial(fmt.Sprintf("ws://%s:%d", Host, WsPort), nil) 22 | ws = c 23 | if err != nil { 24 | log.Panic("dial:", err) 25 | } 26 | defer c.Close() 27 | 28 | for { 29 | _, message, err := c.ReadMessage() 30 | if err != nil { 31 | log.Fatal("read:", err) 32 | break 33 | } 34 | j := make(map[string]string) 35 | json.Unmarshal(message, &j) 36 | 37 | updateMsgChan <- AppendLogMsg(time.Now().Format("[2006-01-02 15:04:05]") + " " + j["content"]) 38 | } 39 | } 40 | 41 | func SendMessage(msg string) error { 42 | return ws.WriteMessage(websocket.TextMessage, []byte(msg)) 43 | } 44 | -------------------------------------------------------------------------------- /jbs-client/ui/constants.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "encoding/json" 5 | "math/rand" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/thoas/go-funk" 11 | ) 12 | 13 | const ( 14 | textinputType = 0 15 | textareaType = 1 16 | ) 17 | 18 | var ( 19 | menu = make([]Function, 0) 20 | ) 21 | 22 | type ParamChecker interface { 23 | Check(string) bool 24 | } 25 | 26 | type Function struct { 27 | Name string 28 | Params []struct { 29 | Name string 30 | InputType int 31 | Checker func(string) bool 32 | Value string 33 | } 34 | ToJSON func([]string) string 35 | } 36 | 37 | func ClassAndMethodChecker(s string) bool { return len(strings.Split(s, "#")) == 2 } 38 | 39 | func CommonMap() map[string]interface{} { 40 | m := make(map[string]interface{}) 41 | m["id"] = randomString(4) 42 | m["timestamp"] = time.Now().UnixMilli() 43 | return m 44 | } 45 | 46 | func WatchToJSON(params []string) string { 47 | m := CommonMap() 48 | m["type"] = "WATCH" 49 | m["signature"] = params[0] 50 | minCost, _ := strconv.Atoi(params[1]) 51 | m["minCost"] = minCost 52 | str, _ := json.Marshal(m) 53 | return string(str) 54 | } 55 | 56 | func OuterWatchToJSON(params []string) string { 57 | m := CommonMap() 58 | m["type"] = "OUTER_WATCH" 59 | m["signature"] = params[0] 60 | m["innerSignature"] = params[1] 61 | str, _ := json.Marshal(m) 62 | return string(str) 63 | } 64 | 65 | func TraceToJSON(params []string) string { 66 | m := CommonMap() 67 | m["type"] = "TRACE" 68 | m["signature"] = params[0] 69 | minCost, _ := strconv.Atoi(params[1]) 70 | m["minCost"] = minCost 71 | ignoreZero, _ := strconv.ParseBool(params[2]) 72 | m["ignoreZero"] = ignoreZero 73 | str, _ := json.Marshal(m) 74 | return string(str) 75 | } 76 | 77 | func ChangeBodyToJSON(params []string) string { 78 | m := CommonMap() 79 | m["type"] = "CHANGE_BODY" 80 | m["className"] = strings.Split(params[0], "#")[0] 81 | m["method"] = strings.Split(params[0], "#")[1] 82 | m["paramTypes"] = funk.Map(strings.Split(params[1], ","), func(s string) string { 83 | return strings.TrimSpace(s) 84 | }).([]string) 85 | m["body"] = params[2] 86 | str, _ := json.Marshal(m) 87 | return string(str) 88 | } 89 | 90 | func ChangeResultToJSON(params []string) string { 91 | m := CommonMap() 92 | m["type"] = "CHANGE_RESULT" 93 | m["className"] = strings.Split(params[0], "#")[0] 94 | m["method"] = strings.Split(params[0], "#")[1] 95 | m["paramTypes"] = funk.Map(strings.Split(params[1], ","), func(s string) string { 96 | return strings.TrimSpace(s) 97 | }).([]string) 98 | m["innerClassName"] = strings.Split(params[2], "#")[0] 99 | m["innerMethod"] = strings.Split(params[2], "#")[1] 100 | m["body"] = params[3] 101 | str, _ := json.Marshal(m) 102 | return string(str) 103 | } 104 | 105 | func ExecToJSON(params []string) string { 106 | m := CommonMap() 107 | m["body"] = `package w; 108 | import w.Global; 109 | import w.util.SpringUtils; 110 | import org.springframework.context.ApplicationContext; 111 | import java.util.*; 112 | public class Exec{ 113 | public void exec() {` + params[0] + `} 114 | }` 115 | m["type"] = "EXEC" 116 | str, _ := json.Marshal(m) 117 | return string(str) 118 | } 119 | 120 | func ResetToJSON(params []string) string { 121 | m := CommonMap() 122 | m["type"] = "RESET" 123 | str, _ := json.Marshal(m) 124 | return string(str) 125 | } 126 | 127 | func init() { 128 | rand.Seed(time.Now().UnixNano()) 129 | watch := Function{"Watch", []struct { 130 | Name string 131 | InputType int 132 | Checker func(s string) bool 133 | Value string 134 | }{ 135 | {"ClassName#MethodName", 0, ClassAndMethodChecker, ""}, 136 | {"MinCost", 0, func(s string) bool { return true }, "0"}, 137 | }, WatchToJSON} 138 | 139 | outerWatch := Function{"OuterWatch", []struct { 140 | Name string 141 | InputType int 142 | Checker func(s string) bool 143 | Value string 144 | }{ 145 | {"ClassName#MethodName", 0, ClassAndMethodChecker, ""}, 146 | {"InnerClassName#InnerMethodName", 0, ClassAndMethodChecker, ""}, 147 | }, OuterWatchToJSON} 148 | 149 | trace := Function{"Trace", []struct { 150 | Name string 151 | InputType int 152 | Checker func(s string) bool 153 | Value string 154 | }{ 155 | {"ClassName#MethodName", 0, ClassAndMethodChecker, ""}, 156 | {"MinCost", 0, func(s string) bool { return true }, "0"}, 157 | {"IgnoreSubMethodZeroCost", 0, func(s string) bool { return true }, "true"}, 158 | }, TraceToJSON} 159 | 160 | changeBody := Function{"ChangeBody", []struct { 161 | Name string 162 | InputType int 163 | Checker func(s string) bool 164 | Value string 165 | }{ 166 | {"ClassName#MethodName", 0, ClassAndMethodChecker, ""}, 167 | {"ParamTypes", 0, func(s string) bool { return true }, ""}, 168 | {"Body", 1, func(s string) bool { return true }, ""}, 169 | }, ChangeBodyToJSON} 170 | 171 | changeResult := Function{"ChangeResult", []struct { 172 | Name string 173 | InputType int 174 | Checker func(s string) bool 175 | Value string 176 | }{ 177 | {"ClassName#MethodName", 0, ClassAndMethodChecker, ""}, 178 | {"ParamTypes", 0, func(s string) bool { return true }, ""}, 179 | {"InnerClassName#InnerMethodName", 0, ClassAndMethodChecker, ""}, 180 | {"Body", 1, func(s string) bool { return true }, ""}, 181 | }, ChangeResultToJSON} 182 | 183 | exec := Function{"Exec", []struct { 184 | Name string 185 | InputType int 186 | Checker func(s string) bool 187 | Value string 188 | }{ 189 | {"Body", 1, func(s string) bool { return true }, ` 190 | try { 191 | ApplicationContext ctx = 192 | (ApplicationContext) SpringUtils.getSpringBootApplicationContext(); 193 | Global.info(Arrays.toString(ctx.getBeanDefinitionNames())); 194 | } catch (Exception e) { 195 | Global.error(e.toString(), e); 196 | } 197 | `}, 198 | }, ExecToJSON} 199 | 200 | reset := Function{"Reset", []struct { 201 | Name string 202 | InputType int 203 | Checker func(s string) bool 204 | Value string 205 | }{}, ResetToJSON} 206 | 207 | menu = []Function{watch, outerWatch, trace, changeBody, changeResult, exec, reset} 208 | 209 | } 210 | 211 | const letterNumberBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 212 | 213 | func randomString(n int) string { 214 | b := make([]byte, n) 215 | for i := range b { 216 | b[i] = letterNumberBytes[rand.Intn(len(letterNumberBytes))] 217 | } 218 | return string(b) 219 | } 220 | -------------------------------------------------------------------------------- /jbs-client/ui/input_container.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | 8 | "github.com/charmbracelet/bubbles/list" 9 | "github.com/charmbracelet/bubbles/textarea" 10 | "github.com/charmbracelet/bubbles/textinput" 11 | tea "github.com/charmbracelet/bubbletea" 12 | "github.com/charmbracelet/lipgloss" 13 | "github.com/sunwu51/jbs/client/request" 14 | ) 15 | 16 | var ( 17 | focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) 18 | blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) 19 | focusedButton = focusedStyle.Render("[ Submit ]") 20 | blurredButton = blurredStyle.Render("[ Submit ]") 21 | ) 22 | 23 | type InputContainer struct { 24 | chooseMenu list.Model 25 | inputMenu inputs 26 | level int 27 | width int 28 | } 29 | 30 | type inputs struct { 31 | focusIndex int 32 | labels []string 33 | inputs []inputModel 34 | } 35 | 36 | type inputModel interface { 37 | Focus() tea.Cmd 38 | Blur() 39 | View() string 40 | Value() string 41 | } 42 | type listItem string 43 | 44 | type listItemDelegate struct{} 45 | 46 | type chooseCursorMsg int 47 | type gotoMainMenu struct{} 48 | 49 | func (i listItem) FilterValue() string { return "" } 50 | 51 | func (d listItemDelegate) Height() int { return 1 } 52 | func (d listItemDelegate) Spacing() int { return 0 } 53 | func (d listItemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } 54 | func (d listItemDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) { 55 | i, ok := item.(listItem) 56 | if !ok { 57 | return 58 | } 59 | str := " " + fmt.Sprintf("%d. %s", index+1, i) 60 | if index == m.Index() { 61 | str = focusedStyle.Copy().Bold(true). 62 | Render("> " + fmt.Sprintf("%d. %s", index+1, i)) 63 | } 64 | fmt.Fprint(w, str) 65 | } 66 | 67 | // ========inputs: a custom ui component with multi inputs and labels 68 | func (m inputs) Init() tea.Cmd { 69 | return func() tea.Msg { 70 | return chooseCursorMsg(0) 71 | } 72 | } 73 | 74 | func (m inputs) Update(msg tea.Msg) (inputs, tea.Cmd) { 75 | cmds := make([]tea.Cmd, len(m.inputs)) 76 | switch msg := msg.(type) { 77 | case tea.KeyMsg: 78 | switch msg.String() { 79 | case "esc": 80 | return m, func() tea.Msg { return gotoMainMenu{} } 81 | case "tab", "shift+tab": 82 | if msg.String() == "tab" { 83 | m.focusIndex = (m.focusIndex + 1) % (len(m.inputs) + 1) 84 | } else { 85 | m.focusIndex = (m.focusIndex - 1) % (len(m.inputs) + 1) 86 | } 87 | for i, inp := range m.inputs { 88 | if i == m.focusIndex { 89 | cmds[i] = inp.Focus() 90 | m.inputs[i] = inp 91 | } else { 92 | inp.Blur() 93 | m.inputs[i] = inp 94 | } 95 | } 96 | return m, tea.Batch(cmds...) 97 | } 98 | } 99 | return m, m.updateInputs(msg) 100 | } 101 | 102 | func (m *inputs) updateInputs(msg tea.Msg) tea.Cmd { 103 | cmds := make([]tea.Cmd, len(m.inputs)) 104 | for i := range m.inputs { 105 | inp := m.inputs[i] 106 | if _, ok := inp.(*textinput.Model); ok { 107 | _i, _c := inp.(*textinput.Model).Update(msg) 108 | inp = &_i 109 | cmds[i] = _c 110 | 111 | } else { 112 | _i, _c := inp.(*textarea.Model).Update(msg) 113 | inp = &_i 114 | cmds[i] = _c 115 | } 116 | m.inputs[i] = inp 117 | } 118 | return tea.Batch(cmds...) 119 | } 120 | 121 | func (m inputs) View() string { 122 | var b strings.Builder 123 | for i := range m.inputs { 124 | b.WriteString(m.labels[i] + "\n") 125 | if i == m.focusIndex { 126 | m.inputs[i].Focus() 127 | if _, ok := m.inputs[i].(*textinput.Model); ok { 128 | b.WriteString(focusedStyle.Render(m.inputs[i].View())) 129 | } else { 130 | area := m.inputs[i].(*textarea.Model) 131 | area.FocusedStyle = textarea.Style{ 132 | CursorLine: focusedStyle, 133 | Text: focusedStyle, 134 | LineNumber: focusedStyle, 135 | } 136 | b.WriteString(m.inputs[i].View()) 137 | } 138 | } else { 139 | m.inputs[i].Blur() 140 | b.WriteString(m.inputs[i].View()) 141 | } 142 | b.WriteRune('\n') 143 | } 144 | 145 | button := &blurredButton 146 | if m.focusIndex == len(m.inputs) { 147 | button = &focusedButton 148 | } 149 | fmt.Fprintf(&b, "\n\n%s\n", *button) 150 | b.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#0F0")).Render("press esc go back")) 151 | return b.String() 152 | } 153 | 154 | // ======InputContainer: a custom ui component combined by 2 components: 155 | // 156 | // a choose list list.Model and a inputs, when level=0 choose list is active and showed 157 | // when level=1 the inputs is active and choose list hides 158 | func (m InputContainer) Init() tea.Cmd { 159 | return nil 160 | } 161 | 162 | func (m InputContainer) Update(msg tea.Msg) (InputContainer, tea.Cmd) { 163 | var cmd tea.Cmd 164 | if m.level == 0 { 165 | switch msg := msg.(type) { 166 | case tea.WindowSizeMsg: 167 | m.width = msg.Width 168 | case tea.KeyMsg: 169 | switch msg.Type { 170 | case tea.KeyTab: 171 | if m.chooseMenu.Cursor() == len(menu)-1 { 172 | m.chooseMenu.Select(0) 173 | } else { 174 | m.chooseMenu.CursorDown() 175 | } 176 | case tea.KeyShiftTab: 177 | if m.chooseMenu.Cursor() == 0 { 178 | m.chooseMenu.Select(len(menu) - 1) 179 | } else { 180 | m.chooseMenu.CursorUp() 181 | } 182 | case tea.KeyEnter: 183 | m.level = 1 184 | m.inputMenu.focusIndex = 0 185 | params := menu[int(m.chooseMenu.Cursor())].Params 186 | inputs := make([]inputModel, len(params)) 187 | labels := make([]string, len(params)) 188 | for i := range inputs { 189 | labels[i] = params[i].Name 190 | if params[i].InputType == textareaType { 191 | t := textarea.New() 192 | t.SetWidth(m.width/2 - 1) 193 | t.SetHeight(25) 194 | if i == 0 { 195 | t.Focus() 196 | } 197 | t.SetValue(params[i].Value) 198 | inputs[i] = &t 199 | } else if params[i].InputType == textinputType { 200 | t := textinput.New() 201 | if i == 0 { 202 | t.Focus() 203 | } 204 | t.SetValue(params[i].Value) 205 | inputs[i] = &t 206 | } 207 | } 208 | m.inputMenu.inputs = inputs 209 | m.inputMenu.labels = labels 210 | return m, nil 211 | 212 | } 213 | } 214 | return m, func() tea.Msg { 215 | return chooseCursorMsg(m.chooseMenu.Cursor()) 216 | } 217 | } else { 218 | switch msg := msg.(type) { 219 | case tea.KeyMsg: 220 | // submit enter 221 | if m.inputMenu.focusIndex == len(m.inputMenu.inputs) && msg.Type == tea.KeyEnter { 222 | vals := []string{} 223 | for _, inp := range m.inputMenu.inputs { 224 | vals = append(vals, inp.Value()) 225 | } 226 | for i, p := range menu[m.chooseMenu.Cursor()].Params { 227 | if !p.Checker(m.inputMenu.inputs[i].Value()) { 228 | return m, func() tea.Msg { return request.AppendLogMsg("Param Invalid") } 229 | } 230 | } 231 | request.SendMessage(menu[m.chooseMenu.Cursor()].ToJSON(vals)) 232 | } 233 | case gotoMainMenu: 234 | m.level = 0 235 | } 236 | m.inputMenu, cmd = m.inputMenu.Update(msg) 237 | } 238 | 239 | return m, cmd 240 | } 241 | 242 | func (m InputContainer) View() string { 243 | if m.level == 0 { 244 | return m.chooseMenu.View() 245 | } 246 | return m.inputMenu.View() 247 | } 248 | 249 | func NewInputContainer() InputContainer { 250 | items := make([]list.Item, 0) 251 | for _, k := range menu { 252 | items = append(items, listItem(k.Name)) 253 | } 254 | chooseMenu := list.New(items, listItemDelegate{}, 30, 14) 255 | chooseMenu.Title = "Input the action?" 256 | chooseMenu.Styles.Title = focusedStyle 257 | chooseMenu.SetShowHelp(false) 258 | chooseMenu.SetShowStatusBar(false) 259 | chooseMenu.SetFilteringEnabled(false) 260 | return InputContainer{ 261 | level: 0, 262 | chooseMenu: chooseMenu, 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /jbs-client/ui/log_container.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/charmbracelet/bubbles/textarea" 7 | tea "github.com/charmbracelet/bubbletea" 8 | "github.com/charmbracelet/lipgloss" 9 | "github.com/sunwu51/jbs/client/request" 10 | ) 11 | 12 | const logMaxLength = 200 13 | 14 | type LogContainer struct { 15 | messages []string 16 | text textarea.Model 17 | } 18 | 19 | func (m LogContainer) Init() tea.Cmd { 20 | return nil 21 | } 22 | 23 | func (m LogContainer) Update(msg tea.Msg) (LogContainer, tea.Cmd) { 24 | switch msg := msg.(type) { 25 | case request.AppendLogMsg: 26 | m.messages = append([]string{string(msg)}, m.messages...) 27 | str := strings.Join(m.messages, "\n") 28 | if len(m.messages) > logMaxLength { 29 | m.messages = m.messages[0:logMaxLength] 30 | } 31 | m.text.SetValue(str) 32 | case tea.WindowSizeMsg: 33 | m.text.SetWidth(msg.Width/2 - 4) 34 | m.text.SetHeight(msg.Height - 5) 35 | } 36 | return m, nil 37 | } 38 | 39 | func (m LogContainer) View() string { 40 | st := lipgloss.NewStyle(). 41 | Border(lipgloss.RoundedBorder()). 42 | BorderForeground(lipgloss.Color("#26f7ce")). 43 | BorderBackground(lipgloss.Color("#26f7ce")). 44 | Padding(1) 45 | return st.Render(m.text.View()) 46 | } 47 | 48 | func NewLogContainer() LogContainer { 49 | text := textarea.New() 50 | text.SetHeight(25) 51 | text.ShowLineNumbers = false 52 | text.Prompt = "" 53 | text.Blur() 54 | text.CharLimit = -1 55 | return LogContainer{ 56 | messages: []string{}, 57 | text: text, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.2.0 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | # e.g. to debug Maven itself, use 32 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | # ---------------------------------------------------------------------------- 35 | 36 | if [ -z "$MAVEN_SKIP_RC" ] ; then 37 | 38 | if [ -f /usr/local/etc/mavenrc ] ; then 39 | . /usr/local/etc/mavenrc 40 | fi 41 | 42 | if [ -f /etc/mavenrc ] ; then 43 | . /etc/mavenrc 44 | fi 45 | 46 | if [ -f "$HOME/.mavenrc" ] ; then 47 | . "$HOME/.mavenrc" 48 | fi 49 | 50 | fi 51 | 52 | # OS specific support. $var _must_ be set to either true or false. 53 | cygwin=false; 54 | darwin=false; 55 | mingw=false 56 | case "$(uname)" in 57 | CYGWIN*) cygwin=true ;; 58 | MINGW*) mingw=true;; 59 | Darwin*) darwin=true 60 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 61 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 62 | if [ -z "$JAVA_HOME" ]; then 63 | if [ -x "/usr/libexec/java_home" ]; then 64 | JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME 65 | else 66 | JAVA_HOME="/Library/Java/Home"; export JAVA_HOME 67 | fi 68 | fi 69 | ;; 70 | esac 71 | 72 | if [ -z "$JAVA_HOME" ] ; then 73 | if [ -r /etc/gentoo-release ] ; then 74 | JAVA_HOME=$(java-config --jre-home) 75 | fi 76 | fi 77 | 78 | # For Cygwin, ensure paths are in UNIX format before anything is touched 79 | if $cygwin ; then 80 | [ -n "$JAVA_HOME" ] && 81 | JAVA_HOME=$(cygpath --unix "$JAVA_HOME") 82 | [ -n "$CLASSPATH" ] && 83 | CLASSPATH=$(cygpath --path --unix "$CLASSPATH") 84 | fi 85 | 86 | # For Mingw, ensure paths are in UNIX format before anything is touched 87 | if $mingw ; then 88 | [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && 89 | JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" 90 | fi 91 | 92 | if [ -z "$JAVA_HOME" ]; then 93 | javaExecutable="$(which javac)" 94 | if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then 95 | # readlink(1) is not available as standard on Solaris 10. 96 | readLink=$(which readlink) 97 | if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then 98 | if $darwin ; then 99 | javaHome="$(dirname "\"$javaExecutable\"")" 100 | javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" 101 | else 102 | javaExecutable="$(readlink -f "\"$javaExecutable\"")" 103 | fi 104 | javaHome="$(dirname "\"$javaExecutable\"")" 105 | javaHome=$(expr "$javaHome" : '\(.*\)/bin') 106 | JAVA_HOME="$javaHome" 107 | export JAVA_HOME 108 | fi 109 | fi 110 | fi 111 | 112 | if [ -z "$JAVACMD" ] ; then 113 | if [ -n "$JAVA_HOME" ] ; then 114 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 115 | # IBM's JDK on AIX uses strange locations for the executables 116 | JAVACMD="$JAVA_HOME/jre/sh/java" 117 | else 118 | JAVACMD="$JAVA_HOME/bin/java" 119 | fi 120 | else 121 | JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" 122 | fi 123 | fi 124 | 125 | if [ ! -x "$JAVACMD" ] ; then 126 | echo "Error: JAVA_HOME is not defined correctly." >&2 127 | echo " We cannot execute $JAVACMD" >&2 128 | exit 1 129 | fi 130 | 131 | if [ -z "$JAVA_HOME" ] ; then 132 | echo "Warning: JAVA_HOME environment variable is not set." 133 | fi 134 | 135 | # traverses directory structure from process work directory to filesystem root 136 | # first directory with .mvn subdirectory is considered project base directory 137 | find_maven_basedir() { 138 | if [ -z "$1" ] 139 | then 140 | echo "Path not specified to find_maven_basedir" 141 | return 1 142 | fi 143 | 144 | basedir="$1" 145 | wdir="$1" 146 | while [ "$wdir" != '/' ] ; do 147 | if [ -d "$wdir"/.mvn ] ; then 148 | basedir=$wdir 149 | break 150 | fi 151 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 152 | if [ -d "${wdir}" ]; then 153 | wdir=$(cd "$wdir/.." || exit 1; pwd) 154 | fi 155 | # end of workaround 156 | done 157 | printf '%s' "$(cd "$basedir" || exit 1; pwd)" 158 | } 159 | 160 | # concatenates all lines of a file 161 | concat_lines() { 162 | if [ -f "$1" ]; then 163 | # Remove \r in case we run on Windows within Git Bash 164 | # and check out the repository with auto CRLF management 165 | # enabled. Otherwise, we may read lines that are delimited with 166 | # \r\n and produce $'-Xarg\r' rather than -Xarg due to word 167 | # splitting rules. 168 | tr -s '\r\n' ' ' < "$1" 169 | fi 170 | } 171 | 172 | log() { 173 | if [ "$MVNW_VERBOSE" = true ]; then 174 | printf '%s\n' "$1" 175 | fi 176 | } 177 | 178 | BASE_DIR=$(find_maven_basedir "$(dirname "$0")") 179 | if [ -z "$BASE_DIR" ]; then 180 | exit 1; 181 | fi 182 | 183 | MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR 184 | log "$MAVEN_PROJECTBASEDIR" 185 | 186 | ########################################################################################## 187 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 188 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 189 | ########################################################################################## 190 | wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" 191 | if [ -r "$wrapperJarPath" ]; then 192 | log "Found $wrapperJarPath" 193 | else 194 | log "Couldn't find $wrapperJarPath, downloading it ..." 195 | 196 | if [ -n "$MVNW_REPOURL" ]; then 197 | wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 198 | else 199 | wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 200 | fi 201 | while IFS="=" read -r key value; do 202 | # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) 203 | safeValue=$(echo "$value" | tr -d '\r') 204 | case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; 205 | esac 206 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" 207 | log "Downloading from: $wrapperUrl" 208 | 209 | if $cygwin; then 210 | wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") 211 | fi 212 | 213 | if command -v wget > /dev/null; then 214 | log "Found wget ... using wget" 215 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" 216 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 217 | wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 218 | else 219 | wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 220 | fi 221 | elif command -v curl > /dev/null; then 222 | log "Found curl ... using curl" 223 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" 224 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 225 | curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" 226 | else 227 | curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" 228 | fi 229 | else 230 | log "Falling back to using Java to download" 231 | javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" 232 | javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" 233 | # For Cygwin, switch paths to Windows format before running javac 234 | if $cygwin; then 235 | javaSource=$(cygpath --path --windows "$javaSource") 236 | javaClass=$(cygpath --path --windows "$javaClass") 237 | fi 238 | if [ -e "$javaSource" ]; then 239 | if [ ! -e "$javaClass" ]; then 240 | log " - Compiling MavenWrapperDownloader.java ..." 241 | ("$JAVA_HOME/bin/javac" "$javaSource") 242 | fi 243 | if [ -e "$javaClass" ]; then 244 | log " - Running MavenWrapperDownloader.java ..." 245 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" 246 | fi 247 | fi 248 | fi 249 | fi 250 | ########################################################################################## 251 | # End of extension 252 | ########################################################################################## 253 | 254 | # If specified, validate the SHA-256 sum of the Maven wrapper jar file 255 | wrapperSha256Sum="" 256 | while IFS="=" read -r key value; do 257 | case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; 258 | esac 259 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" 260 | if [ -n "$wrapperSha256Sum" ]; then 261 | wrapperSha256Result=false 262 | if command -v sha256sum > /dev/null; then 263 | if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then 264 | wrapperSha256Result=true 265 | fi 266 | elif command -v shasum > /dev/null; then 267 | if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then 268 | wrapperSha256Result=true 269 | fi 270 | else 271 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." 272 | echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." 273 | exit 1 274 | fi 275 | if [ $wrapperSha256Result = false ]; then 276 | echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 277 | echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 278 | echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 279 | exit 1 280 | fi 281 | fi 282 | 283 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 284 | 285 | # For Cygwin, switch paths to Windows format before running java 286 | if $cygwin; then 287 | [ -n "$JAVA_HOME" ] && 288 | JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") 289 | [ -n "$CLASSPATH" ] && 290 | CLASSPATH=$(cygpath --path --windows "$CLASSPATH") 291 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 292 | MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") 293 | fi 294 | 295 | # Provide a "standardized" way to retrieve the CLI args that will 296 | # work with both Windows and non-Windows executions. 297 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" 298 | export MAVEN_CMD_LINE_ARGS 299 | 300 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 301 | 302 | # shellcheck disable=SC2086 # safe args 303 | exec "$JAVACMD" \ 304 | $MAVEN_OPTS \ 305 | $MAVEN_DEBUG_OPTS \ 306 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 307 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 308 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 309 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Apache Maven Wrapper startup batch script, version 3.2.0 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 28 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 29 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 30 | @REM e.g. to debug Maven itself, use 31 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 32 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 33 | @REM ---------------------------------------------------------------------------- 34 | 35 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 36 | @echo off 37 | @REM set title of command window 38 | title %0 39 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 40 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 41 | 42 | @REM set %HOME% to equivalent of $HOME 43 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 44 | 45 | @REM Execute a user defined script before this one 46 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 47 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 48 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 49 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 50 | :skipRcPre 51 | 52 | @setlocal 53 | 54 | set ERROR_CODE=0 55 | 56 | @REM To isolate internal variables from possible post scripts, we use another setlocal 57 | @setlocal 58 | 59 | @REM ==== START VALIDATION ==== 60 | if not "%JAVA_HOME%" == "" goto OkJHome 61 | 62 | echo. 63 | echo Error: JAVA_HOME not found in your environment. >&2 64 | echo Please set the JAVA_HOME variable in your environment to match the >&2 65 | echo location of your Java installation. >&2 66 | echo. 67 | goto error 68 | 69 | :OkJHome 70 | if exist "%JAVA_HOME%\bin\java.exe" goto init 71 | 72 | echo. 73 | echo Error: JAVA_HOME is set to an invalid directory. >&2 74 | echo JAVA_HOME = "%JAVA_HOME%" >&2 75 | echo Please set the JAVA_HOME variable in your environment to match the >&2 76 | echo location of your Java installation. >&2 77 | echo. 78 | goto error 79 | 80 | @REM ==== END VALIDATION ==== 81 | 82 | :init 83 | 84 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 85 | @REM Fallback to current working directory if not found. 86 | 87 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 88 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 89 | 90 | set EXEC_DIR=%CD% 91 | set WDIR=%EXEC_DIR% 92 | :findBaseDir 93 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 94 | cd .. 95 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 96 | set WDIR=%CD% 97 | goto findBaseDir 98 | 99 | :baseDirFound 100 | set MAVEN_PROJECTBASEDIR=%WDIR% 101 | cd "%EXEC_DIR%" 102 | goto endDetectBaseDir 103 | 104 | :baseDirNotFound 105 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 106 | cd "%EXEC_DIR%" 107 | 108 | :endDetectBaseDir 109 | 110 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 111 | 112 | @setlocal EnableExtensions EnableDelayedExpansion 113 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 114 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 115 | 116 | :endReadAdditionalConfig 117 | 118 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 123 | 124 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 125 | IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | if "%MVNW_VERBOSE%" == "true" ( 132 | echo Found %WRAPPER_JAR% 133 | ) 134 | ) else ( 135 | if not "%MVNW_REPOURL%" == "" ( 136 | SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" 137 | ) 138 | if "%MVNW_VERBOSE%" == "true" ( 139 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 140 | echo Downloading from: %WRAPPER_URL% 141 | ) 142 | 143 | powershell -Command "&{"^ 144 | "$webclient = new-object System.Net.WebClient;"^ 145 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 146 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 147 | "}"^ 148 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ 149 | "}" 150 | if "%MVNW_VERBOSE%" == "true" ( 151 | echo Finished downloading %WRAPPER_JAR% 152 | ) 153 | ) 154 | @REM End of extension 155 | 156 | @REM If specified, validate the SHA-256 sum of the Maven wrapper jar file 157 | SET WRAPPER_SHA_256_SUM="" 158 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 159 | IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B 160 | ) 161 | IF NOT %WRAPPER_SHA_256_SUM%=="" ( 162 | powershell -Command "&{"^ 163 | "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ 164 | "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ 165 | " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ 166 | " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ 167 | " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ 168 | " exit 1;"^ 169 | "}"^ 170 | "}" 171 | if ERRORLEVEL 1 goto error 172 | ) 173 | 174 | @REM Provide a "standardized" way to retrieve the CLI args that will 175 | @REM work with both Windows and non-Windows executions. 176 | set MAVEN_CMD_LINE_ARGS=%* 177 | 178 | %MAVEN_JAVA_EXE% ^ 179 | %JVM_CONFIG_MAVEN_PROPS% ^ 180 | %MAVEN_OPTS% ^ 181 | %MAVEN_DEBUG_OPTS% ^ 182 | -classpath %WRAPPER_JAR% ^ 183 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 184 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 185 | if ERRORLEVEL 1 goto error 186 | goto end 187 | 188 | :error 189 | set ERROR_CODE=1 190 | 191 | :end 192 | @endlocal & set ERROR_CODE=%ERROR_CODE% 193 | 194 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 195 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 196 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 197 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 198 | :skipRcPost 199 | 200 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 201 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 202 | 203 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 204 | 205 | cmd /C exit /B %ERROR_CODE% 206 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | w 6 | swapper 7 | 0.0.1-SNAPSHOT 8 | swapper 9 | swapper 10 | 11 | 1.8 12 | 13 | 14 | 15 | com.sun 16 | tools 17 | 1.8 18 | system 19 | ${JAVA_HOME}/lib/tools.jar 20 | 21 | 22 | org.nanohttpd 23 | nanohttpd 24 | 2.2.0 25 | 26 | 27 | 28 | org.nanohttpd 29 | nanohttpd-websocket 30 | 2.2.0 31 | 32 | 33 | 34 | org.benf 35 | cfr 36 | 0.152 37 | 38 | 39 | 40 | 41 | org.ow2.asm 42 | asm-commons 43 | 9.7 44 | 45 | 46 | 47 | org.ow2.asm 48 | asm 49 | 9.7 50 | 51 | 52 | 53 | org.codehaus.janino 54 | janino 55 | 3.1.12 56 | 57 | 58 | 59 | ognl 60 | ognl 61 | 3.2.1 62 | 63 | 64 | org.projectlombok 65 | lombok 66 | 1.18.28 67 | true 68 | 69 | 70 | com.fasterxml.jackson.core 71 | jackson-databind 72 | 2.13.5 73 | 74 | 75 | org.apache.groovy 76 | groovy 77 | 4.0.22 78 | 79 | 80 | org.apache.groovy 81 | groovy-json 82 | 4.0.22 83 | 84 | 85 | org.apache.groovy 86 | groovy-jsr223 87 | 4.0.22 88 | 89 | 90 | com.fasterxml.jackson.datatype 91 | jackson-datatype-jsr310 92 | 2.13.5 93 | 94 | 95 | org.junit.jupiter 96 | junit-jupiter 97 | 5.8.1 98 | test 99 | 100 | 101 | net.bytebuddy 102 | byte-buddy-agent 103 | 1.14.11 104 | test 105 | 106 | 107 | 108 | 109 | 110 | org.apache.maven.plugins 111 | maven-dependency-plugin 112 | 3.3.0 113 | 114 | 115 | copy-dependencies 116 | prepare-package 117 | 118 | copy-dependencies 119 | 120 | 121 | ${project.build.directory}/classes/W-INF/lib 122 | org.apache.groovy 123 | false 124 | false 125 | true 126 | 127 | 128 | 129 | 130 | 131 | org.apache.maven.plugins 132 | maven-shade-plugin 133 | 3.5.0 134 | 135 | 136 | 137 | org.apache.groovy:* 138 | 139 | 140 | 141 | 142 | *:* 143 | 144 | META-INF/*.DSA 145 | META-INF/*.RSA 146 | META-INF/*.SF 147 | META-INF/LICENSE 148 | META-INF/services/com.fasterxml.jackson* 149 | META-INF/versions/**/* 150 | *.md 151 | AUTHORS 152 | LICENSE 153 | *.txt 154 | *.html 155 | *.properties 156 | 157 | 158 | 159 | 160 | 161 | 162 | w.Attach 163 | w.App 164 | true 165 | 166 | 167 | 168 | 169 | 170 | com.fasterxml.jackson 171 | wshade.com.fasterxml.jackson 172 | 173 | 174 | org.objectweb.asm 175 | wshade.org.objectweb.asm 176 | 177 | 178 | org.codehaus.janino 179 | wshade.org.codehaus.janino 180 | 181 | 182 | org.codehaus.commons.compiler 183 | wshade.org.codehaus.commons.compiler 184 | 185 | 186 | 187 | 188 | 189 | package 190 | 191 | shade 192 | 193 | 194 | 195 | 196 | 197 | org.apache.maven.plugins 198 | maven-compiler-plugin 199 | 200 | 8 201 | 8 202 | 203 | 204 | 205 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /src/main/java/w/App.java: -------------------------------------------------------------------------------- 1 | package w; 2 | 3 | import java.io.*; 4 | import java.lang.instrument.Instrumentation; 5 | import java.lang.reflect.InvocationTargetException; 6 | import java.util.ArrayList; 7 | import java.util.HashSet; 8 | import java.util.List; 9 | import java.util.Set; 10 | import java.util.concurrent.Executors; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | import javassist.*; 14 | 15 | import w.core.ExecBundle; 16 | import w.util.SpringUtils; 17 | import w.web.Httpd; 18 | import w.web.Websocketd; 19 | 20 | public class App { 21 | private static final int DEFAULT_HTTP_PORT = 8000; 22 | private static final int DEFAULT_WEBSOCKET_PORT = 18000; 23 | 24 | public static void agentmain(String arg, Instrumentation instrumentation) throws Exception { 25 | if (arg != null && arg.length() > 0) { 26 | String[] items = arg.split("&"); 27 | for (String item : items) { 28 | String[] kv = item.split("="); 29 | if (kv.length == 2) { 30 | if (System.getProperty(kv[0]) == null) { 31 | System.setProperty(kv[0], kv[1]); 32 | } 33 | } 34 | } 35 | } 36 | Global.instrumentation = instrumentation; 37 | Global.fillLoadedClasses(); 38 | 39 | // 1 record the spring boot classloader 40 | SpringUtils.initFromLoadedClasses(); 41 | 42 | // 2 start http and websocket server 43 | startHttpd(); 44 | startWebsocketd(); 45 | 46 | // 3 init execInstance 47 | initExecInstance(); 48 | 49 | // 4 task to clean closed ws and related enhancer 50 | schedule(); 51 | } 52 | 53 | private static void startHttpd() throws IOException { 54 | int port = DEFAULT_HTTP_PORT; 55 | if (System.getProperty("http_port") != null) { 56 | port = Integer.parseInt(System.getProperty("http_port")); 57 | } 58 | new Httpd(port).start(5000, false); 59 | System.out.println("Http server start at port "+ port); 60 | } 61 | 62 | private static void startWebsocketd() throws IOException { 63 | int port = DEFAULT_WEBSOCKET_PORT; 64 | if (System.getProperty("ws_port") != null) { 65 | port = Integer.parseInt(System.getProperty("ws_port")); 66 | } 67 | new Websocketd(port).start(24 * 60 * 60000, false); 68 | System.out.println("Websocket server start at port " + port); 69 | Global.wsPort = port; 70 | } 71 | 72 | private static void initExecInstance() throws CannotCompileException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, NotFoundException, IOException { 73 | ExecBundle.invoke(); 74 | } 75 | 76 | private static void schedule() { 77 | Executors.newScheduledThreadPool(1) 78 | .scheduleWithFixedDelay(Global::fillLoadedClasses, 5, 60, TimeUnit.SECONDS); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/w/Attach.java: -------------------------------------------------------------------------------- 1 | package w; 2 | 3 | import com.sun.tools.attach.VirtualMachine; 4 | import com.sun.tools.attach.VirtualMachineDescriptor; 5 | import w.util.WClassLoader; 6 | 7 | import java.io.File; 8 | import java.lang.reflect.Method; 9 | import java.net.URL; 10 | import java.nio.file.Paths; 11 | import java.security.CodeSource; 12 | import java.security.ProtectionDomain; 13 | import java.util.Comparator; 14 | import java.util.List; 15 | import java.util.Objects; 16 | import java.util.Scanner; 17 | 18 | /** 19 | * @author Frank 20 | * @date 2023/11/26 13:07 21 | */ 22 | public class Attach { 23 | public static void main(String[] args) throws Exception { 24 | if (!Attach.class.getClassLoader().toString().startsWith(WClassLoader.class.getName())) { 25 | String jdkVersion = System.getProperty("java.version"); 26 | if (jdkVersion.startsWith("1.")) { 27 | if (jdkVersion.startsWith("1.8")) { 28 | try { 29 | // custom class loader to load current jar and tools.jar 30 | WClassLoader customClassLoader = new WClassLoader( 31 | new URL[]{toolsJarUrl(), currentUrl()}, 32 | ClassLoader.getSystemClassLoader().getParent() 33 | ); 34 | Class mainClass = Class.forName("w.Attach", true, customClassLoader); 35 | Method mainMethod = mainClass.getMethod("main", String[].class); 36 | mainMethod.invoke(null, (Object) args); 37 | return; 38 | } catch (Exception e) { 39 | e.printStackTrace(); 40 | System.exit(-1); 41 | } 42 | } else { 43 | Global.error(jdkVersion + " is not supported"); 44 | return; 45 | } 46 | } 47 | } 48 | 49 | // Get the jvm process PID from args[0] or manual input 50 | // And get the spring http port from manual input 51 | String pid = null; 52 | Scanner scanner = new Scanner(System.in); 53 | 54 | if (args.length > 0) { 55 | pid = args[0].trim(); 56 | try { 57 | Integer.parseInt(pid); 58 | } catch (Exception e) { 59 | System.err.println("The pid should be integer."); 60 | throw e; 61 | } 62 | } else { 63 | List jps = VirtualMachine.list(); 64 | jps.sort(Comparator.comparing(VirtualMachineDescriptor::displayName)); 65 | int i = 0; 66 | for (; i < jps.size(); i++) { 67 | System.out.printf("[%s] %s %s%n", i, jps.get(i).id(), jps.get(i).displayName()); 68 | } 69 | System.out.printf("[%s] %s%n", i, "Custom PID"); 70 | System.out.println(">>>>>>>>>>>>Please enter the serial number"); 71 | 72 | while (true) { 73 | int index = scanner.nextInt(); 74 | if (index < 0 || index > i) continue; 75 | if (index == i) { 76 | System.out.println(">>>>>>>>>>>>Please enter the PID"); 77 | pid = String.valueOf(scanner.nextInt()); 78 | break; 79 | } 80 | pid = jps.get(index).id(); 81 | break; 82 | } 83 | } 84 | System.out.printf("============The PID is %s%n", pid); 85 | VirtualMachine jvm = VirtualMachine.attach(pid); 86 | URL jarUrl = Attach.class.getProtectionDomain().getCodeSource().getLocation(); 87 | String curJarPath = Paths.get(jarUrl.toURI()).toString(); 88 | try { 89 | StringBuilder arg = new StringBuilder(); 90 | System.getProperties().forEach((k, v) -> { 91 | if (k.toString().startsWith("w_") && k.toString().length() > 2) { 92 | arg.append(k.toString().substring(2)).append("=").append(v.toString()).append("&"); 93 | } 94 | }); 95 | 96 | jvm.loadAgent(curJarPath, arg.toString()); 97 | jvm.detach(); 98 | } catch (Exception e) { 99 | if (!Objects.equals(e.getMessage(), "0")) { 100 | throw e; 101 | } 102 | } 103 | String port = System.getProperty("w_http_port"); 104 | if (port == null) { 105 | port = "8000"; 106 | } 107 | System.out.println("============Attach finish"); 108 | System.out.println("============Web server started at http://localhost:" + port); 109 | } 110 | 111 | private static URL toolsJarUrl() throws Exception { 112 | String javaHome = System.getProperty("java.home"); 113 | File toolsJarFile = new File(javaHome, "../lib/tools.jar"); 114 | if (!toolsJarFile.exists()) { 115 | throw new Exception("tools.jar not found at: " + toolsJarFile.getPath()); 116 | } 117 | URL toolsJarUrl = toolsJarFile.toURI().toURL(); 118 | return toolsJarUrl; 119 | } 120 | 121 | public static URL currentUrl() throws Exception { 122 | ProtectionDomain domain = Attach.class.getProtectionDomain(); 123 | CodeSource codeSource = domain.getCodeSource(); 124 | return codeSource.getLocation(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/w/core/ExecBundle.java: -------------------------------------------------------------------------------- 1 | package w.core; 2 | 3 | import lombok.Data; 4 | import w.Global; 5 | import w.core.compiler.WCompiler; 6 | import w.web.message.ReplaceClassMessage; 7 | 8 | import java.io.FileInputStream; 9 | import java.lang.reflect.InvocationTargetException; 10 | import java.util.Base64; 11 | import java.util.HashMap; 12 | 13 | /** 14 | * @author Frank 15 | * @date 2023/12/9 20:50 16 | */ 17 | @Data 18 | public class ExecBundle { 19 | private static final String EXEC_CLASS = "w.Exec"; 20 | static Object inst; 21 | 22 | static { 23 | try { 24 | Class c = new ExecClassLoader(w.Global.getClassLoader()).loadClass(EXEC_CLASS); 25 | inst = c.newInstance(); 26 | Global.fillLoadedClasses(); 27 | } catch (Throwable e) { 28 | e.printStackTrace(); 29 | } 30 | } 31 | 32 | public static void invoke() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { 33 | Global.info("start to invoke"); 34 | inst.getClass().getDeclaredMethod("exec") 35 | .invoke(inst); 36 | Global.info("finish invoking"); 37 | } 38 | 39 | public static void changeBodyAndInvoke(String body) throws Exception { 40 | byte[] byteCode = WCompiler.compileWholeClass(body); 41 | ReplaceClassMessage replaceClassMessage = new ReplaceClassMessage(); 42 | replaceClassMessage.setClassName(EXEC_CLASS); 43 | replaceClassMessage.setContent(Base64.getEncoder().encodeToString(byteCode)); 44 | // remove the old transformer 45 | clear(); 46 | if (Swapper.getInstance().swap(replaceClassMessage)) { 47 | invoke(); 48 | } 49 | } 50 | 51 | private static void clear() { 52 | // remove the old transformer 53 | Global.activeTransformers 54 | .getOrDefault(EXEC_CLASS, new HashMap<>()).values().forEach(baseClassTransformers -> { 55 | baseClassTransformers.forEach(transformer -> { 56 | Global.instrumentation.removeTransformer(transformer); 57 | Global.transformers.remove(transformer); 58 | }); 59 | }); 60 | Global.activeTransformers 61 | .getOrDefault(EXEC_CLASS, new HashMap<>()).clear(); 62 | } 63 | 64 | public static class ExecClassLoader extends ClassLoader { 65 | public ExecClassLoader(ClassLoader parent) { 66 | super(parent); 67 | } 68 | 69 | @Override 70 | public Class loadClass(String name) throws ClassNotFoundException { 71 | if (!name.equals(EXEC_CLASS)) { 72 | return super.loadClass(name); 73 | } 74 | try { 75 | byte[] bytes = WCompiler.compileWholeClass("package w; public class Exec { public void exec() {} }"); 76 | return defineClass(EXEC_CLASS, bytes, 0, bytes.length); 77 | } catch (Exception e) { 78 | throw new RuntimeException(e); 79 | } 80 | } 81 | 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /src/main/java/w/core/GroovyBundle.java: -------------------------------------------------------------------------------- 1 | package w.core; 2 | 3 | import groovy.lang.GroovyClassLoader; 4 | import lombok.Data; 5 | import org.codehaus.groovy.jsr223.GroovyScriptEngineImpl; 6 | import w.Global; 7 | import w.util.JarInJarClassLoader; 8 | import w.util.SpringUtils; 9 | 10 | import java.io.BufferedReader; 11 | import java.io.IOException; 12 | import java.io.InputStreamReader; 13 | import java.net.URL; 14 | import java.net.URLClassLoader; 15 | import java.util.ArrayList; 16 | import java.util.Arrays; 17 | import java.util.Enumeration; 18 | import java.util.List; 19 | import java.util.concurrent.TimeUnit; 20 | 21 | import static w.Attach.currentUrl; 22 | 23 | /** 24 | * @author Frank 25 | * @date 2024/7/30 22:58 26 | */ 27 | @Data 28 | public class GroovyBundle { 29 | static ClassLoader cl; 30 | 31 | static Object engineObj; 32 | 33 | static { 34 | try { 35 | JarInJarClassLoader jarInJarClassLoader = 36 | new JarInJarClassLoader(currentUrl(), "W-INF/lib", ClassLoader.getSystemClassLoader().getParent()); 37 | cl = new WGroovyClassLoader(jarInJarClassLoader, Global.getClassLoader()); 38 | Thread.currentThread().setContextClassLoader(cl); 39 | Class engineClass = cl.loadClass("org.codehaus.groovy.jsr223.GroovyScriptEngineImpl"); 40 | Class gclClass = cl.loadClass("groovy.lang.GroovyClassLoader"); 41 | engineObj = engineClass.getConstructor(gclClass).newInstance(gclClass.newInstance()); 42 | engineClass.getMethod("put", String.class, Object.class).invoke(engineObj, "ctx", SpringUtils.getSpringBootApplicationContext()); 43 | } catch (Exception e) { 44 | throw new RuntimeException(e); 45 | } 46 | } 47 | 48 | public static Object eval(String script) throws Exception { 49 | if (script.startsWith("!")) { 50 | return executeCmd(Arrays.asList(script.substring(1).split(" "))); 51 | } else { 52 | return engineObj.getClass().getMethod("eval", String.class).invoke(engineObj, script); 53 | } 54 | } 55 | 56 | private static String executeCmd(List args) throws Exception { 57 | ProcessBuilder builder = new ProcessBuilder(); 58 | List _args = new ArrayList<>(); 59 | String os = System.getProperty("os.name").toLowerCase(); 60 | 61 | if (os.contains("win")) { 62 | _args.add("cmd.exe"); 63 | _args.add("/c"); 64 | } else { 65 | _args.add("sh"); 66 | _args.add("-c"); 67 | } 68 | _args.add(String.join(" ", args)); 69 | builder.command(_args); 70 | Process process = builder.start(); 71 | 72 | StringBuilder sb = new StringBuilder(); 73 | new Thread(() -> { 74 | BufferedReader reader1 = new BufferedReader(new InputStreamReader(process.getInputStream())); 75 | BufferedReader reader2 = new BufferedReader(new InputStreamReader(process.getErrorStream())); 76 | String line1, line2; 77 | while (true) { 78 | try { 79 | if ((line1 = reader1.readLine()) == null) break; 80 | } catch (IOException e) { 81 | throw new RuntimeException(e); 82 | } 83 | sb.append(line1).append('\n'); 84 | } 85 | while (true) { 86 | try { 87 | if ((line2 = reader2.readLine()) == null) break; 88 | } catch (IOException e) { 89 | throw new RuntimeException(e); 90 | } 91 | sb.append(line2).append('\n'); 92 | } 93 | }).start(); 94 | return process.waitFor(10, TimeUnit.SECONDS) ? sb.toString() : "timeout"; 95 | } 96 | 97 | public static class WGroovyClassLoader extends URLClassLoader { 98 | private final ClassLoader delegate; 99 | public WGroovyClassLoader(ClassLoader parent, ClassLoader delegate) throws Exception { 100 | super(new URL[] { currentUrl() }, parent); 101 | this.delegate = Global.getClassLoader(); 102 | } 103 | @Override 104 | public Class loadClass(String name, boolean resolve) throws ClassNotFoundException { 105 | // For entrypoint class, must load it by self 106 | if (name.equals(GroovyBundle.class.getName())) { 107 | Class c = findLoadedClass(name); 108 | if (c != null) return c; 109 | return findClass(name); 110 | } 111 | try { 112 | // For groovy, need to load it by parent(jarInJarClassLoader) 113 | return getParent().loadClass(name); 114 | } catch (ClassNotFoundException e) { 115 | // Else load it by delegate(Global.getClassLoader()) 116 | return delegate.loadClass(name); 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/w/core/Swapper.java: -------------------------------------------------------------------------------- 1 | package w.core; 2 | 3 | import w.*; 4 | import w.core.model.*; 5 | import w.web.message.*; 6 | 7 | import java.lang.reflect.Modifier; 8 | import java.util.*; 9 | 10 | 11 | public class Swapper { 12 | private static final Swapper INSTANCE = new Swapper(); 13 | 14 | private Swapper() {} 15 | 16 | public static Swapper getInstance() { 17 | return INSTANCE; 18 | } 19 | 20 | public boolean swap(Message message) { 21 | BaseClassTransformer transformer = null; 22 | try { 23 | switch (message.getType()) { 24 | case WATCH: 25 | transformer = new WatchTransformer((WatchMessage) message); 26 | break; 27 | case OUTER_WATCH: 28 | transformer = new OuterWatchTransformer((OuterWatchMessage) message); 29 | break; 30 | case CHANGE_BODY: 31 | transformer = new ChangeBodyTransformer((ChangeBodyMessage) message); 32 | break; 33 | case CHANGE_RESULT: 34 | transformer = new ChangeResultTransformer((ChangeResultMessage) message); 35 | break; 36 | case REPLACE_CLASS: 37 | transformer = new ReplaceClassTransformer((ReplaceClassMessage) message); 38 | break; 39 | case TRACE: 40 | transformer = new TraceTransformer((TraceMessage) message); 41 | break; 42 | case DECOMPILE: 43 | transformer = new DecompileTransformer((DecompileMessage) message); 44 | break; 45 | default: 46 | Global.error("type not support"); 47 | throw new RuntimeException("message type not support"); 48 | } 49 | } catch (Throwable e) { 50 | Global.error("build transform error:", e); 51 | return false; 52 | } 53 | 54 | Set> classes = Global.allLoadedClasses.getOrDefault(transformer.getClassName(), new HashSet<>()); 55 | 56 | boolean classExists = false; 57 | for (Class aClass : classes) { 58 | if (transformer instanceof DecompileTransformer) { 59 | // Decompile needn't check abstract 60 | } else if (aClass.isInterface() || Modifier.isAbstract(aClass.getModifiers())) { 61 | Set candidates = new HashSet<>(); 62 | for (Object instances : Global.getInstances(aClass)) { 63 | candidates.add(instances.getClass().getName()); 64 | } 65 | Global.error("!Error: Should use a simple pojo, but " + aClass.getName() + 66 | " is a Interface or Abstract class or something wired, \nmaybe you should use: " + candidates); 67 | return false; 68 | } 69 | classExists = true; 70 | } 71 | 72 | if (!classExists) { 73 | try { 74 | classes.add(Class.forName(transformer.getClassName(), true, Global.getClassLoader())); 75 | } catch (ClassNotFoundException e) { 76 | Global.error("Class not exist: " + transformer.getClassName()); 77 | return false; 78 | } 79 | } 80 | 81 | Global.addTransformer(transformer); 82 | Global.debug("add transformer" + transformer.getUuid() +" finish, will retrans class"); 83 | 84 | for (Class aClass : classes) { 85 | try { 86 | Global.addActiveTransformer(aClass, transformer); 87 | } catch (Throwable e) { 88 | Global.error("re transformer error:", e); 89 | Global.deleteTransformer(transformer.getUuid()); 90 | return false; 91 | } 92 | } 93 | 94 | return true; 95 | } 96 | } 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/main/java/w/core/asm/SbNode.java: -------------------------------------------------------------------------------- 1 | package w.core.asm; 2 | 3 | import org.objectweb.asm.MethodVisitor; 4 | 5 | import static org.objectweb.asm.Opcodes.*; 6 | 7 | /** 8 | * @author Frank 9 | * @date 2024/6/22 18:49 10 | */ 11 | public class SbNode { 12 | String constString; 13 | 14 | int loadType; 15 | 16 | int loadIndex; 17 | 18 | 19 | public SbNode(String constString) { 20 | this.constString = constString; 21 | } 22 | 23 | public SbNode(int loadType, int loadIndex) { 24 | if (loadType != ALOAD && loadType != LLOAD) { 25 | throw new IllegalArgumentException("Unsupported load type in SubStringNode: " + loadType); 26 | } 27 | this.loadType = loadType; 28 | this.loadIndex = loadIndex; 29 | } 30 | 31 | public void loadAndAppend(MethodVisitor mv) { 32 | if (constString != null) { 33 | mv.visitLdcInsn(constString); 34 | mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/Object;)Ljava/lang/StringBuilder;", false); 35 | } else { 36 | mv.visitVarInsn(loadType, loadIndex); 37 | switch (loadType) { 38 | case ALOAD: 39 | mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/Object;)Ljava/lang/StringBuilder;", false); 40 | break; 41 | case LLOAD: 42 | mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false); 43 | break; 44 | default: 45 | throw new IllegalStateException("Unsupported load type in SubStringNode: " + loadType); 46 | } 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/w/core/asm/Tool.java: -------------------------------------------------------------------------------- 1 | package w.core.asm; 2 | 3 | import w.Global; 4 | import w.util.RequestUtils; 5 | 6 | /** 7 | * @author Frank 8 | * @date 2024/6/30 16:27 9 | */ 10 | public class Tool { 11 | public static void watchPostProcess(long startTime, int minCost, String uuid, String traceId, String methodSignature, String params, String result, String exception){ 12 | long cost = System.currentTimeMillis() - startTime; 13 | if (cost >= minCost) { 14 | Global.checkCountAndUnload(uuid); 15 | RequestUtils.fillCurThread(traceId); 16 | Global.info((new StringBuilder()).append(methodSignature) 17 | .append(", cost:").append(cost).append("ms, req:").append(params) 18 | .append(", res:").append(result).append(", throw:").append(exception)); 19 | RequestUtils.clearRequestCtx(); 20 | } 21 | } 22 | 23 | public static void outerWatchPostProcess(int line, long startTime, String uuid, String traceId, String methodSignature, String params, String result, String exception) { 24 | long cost = System.currentTimeMillis() - startTime; 25 | Global.checkCountAndUnload(uuid); 26 | RequestUtils.fillCurThread(traceId); 27 | Global.info(String.format("line: %d, %s, cost: %dms, req: %s, res: %s, throw: %s", line, methodSignature, cost, params, result, exception)); 28 | RequestUtils.clearRequestCtx(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/w/core/asm/WAdviceAdapter.java: -------------------------------------------------------------------------------- 1 | package w.core.asm; 2 | 3 | import org.objectweb.asm.*; 4 | import org.objectweb.asm.commons.AdviceAdapter; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.Stack; 9 | 10 | /** 11 | * @author Frank 12 | * @date 2024/6/22 19:33 13 | */ 14 | public class WAdviceAdapter extends AdviceAdapter { 15 | protected WAdviceAdapter(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) { 16 | super(api, methodVisitor, access, name, descriptor); 17 | } 18 | 19 | /** 20 | * get current milliseconds 21 | * 22 | * long startTime = System.currentTimeMillis(); 23 | * 24 | * @param mv 25 | * @return the start time variable index 26 | */ 27 | protected int asmStoreStartTime(MethodVisitor mv) { 28 | mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false); 29 | int startTimeVarIndex = newLocal(Type.LONG_TYPE); 30 | mv.visitVarInsn(LSTORE, startTimeVarIndex); 31 | return startTimeVarIndex; 32 | } 33 | 34 | /** 35 | * calculate the cost, return cost variable index 36 | * 37 | * long duration = System.currentTimeMillis() - startTime; 38 | * 39 | * @param mv 40 | * @param startTimeVarIndex 41 | * @return the duration time variable index 42 | */ 43 | protected int asmCalculateCost(MethodVisitor mv, int startTimeVarIndex) { 44 | mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false); 45 | mv.visitVarInsn(LLOAD, startTimeVarIndex); 46 | mv.visitInsn(LSUB); 47 | int durationVarIndex = newLocal(Type.LONG_TYPE); 48 | mv.visitVarInsn(LSTORE, durationVarIndex); 49 | return durationVarIndex; 50 | } 51 | 52 | /** 53 | * params to string and return the string variable index 54 | * 55 | * Object[] array = new Object[] {arg1, arg2, arg3...}; 56 | * String paramsVar = null; 57 | * if (printFormat == 1) paramsVar = Arrays.toString(array); 58 | * else if (printFormat == 2) paramsVar = Global.toJson(array); 59 | * else paramsVar = Global.toString(array); 60 | * 61 | * @param mv 62 | * @param printFormat 1 toString 2 toJson 3 toPrettyString 63 | * @return the paramsVar index 64 | */ 65 | protected int asmStoreParamsString(MethodVisitor mv, int printFormat) { 66 | loadArgArray(); 67 | if (printFormat == 1) { 68 | mv.visitMethodInsn(INVOKESTATIC, "java/util/Arrays", "toString", "([Ljava/lang/Object;)Ljava/lang/String;", false); 69 | } else if (printFormat == 2) { 70 | mv.visitMethodInsn(INVOKESTATIC, "w/Global", "toJson", "(Ljava/lang/Object;)Ljava/lang/String;", false); 71 | } else { 72 | mv.visitMethodInsn(INVOKESTATIC, "w/Global", "toString", "(Ljava/lang/Object;)Ljava/lang/String;", false); 73 | } 74 | int paramsVarIndex = newLocal(Type.getType(String.class)); 75 | mv.visitVarInsn(ASTORE, paramsVarIndex); 76 | return paramsVarIndex; 77 | } 78 | 79 | 80 | /** 81 | * sub method params to string and return the string variable index, similar to asmStoreParamsString 82 | * but for the sub method 83 | * @param mv 84 | * @param printFormat 85 | * @param descriptor 86 | * @return 87 | */ 88 | protected int asmSubCallStoreParamsString(MethodVisitor mv, int printFormat, String descriptor) { 89 | int _i = subCallParamsToArray(descriptor); 90 | mv.visitVarInsn(ALOAD, _i); 91 | if (printFormat == 1) { 92 | mv.visitMethodInsn(INVOKESTATIC, "java/util/Arrays", "toString", "([Ljava/lang/Object;)Ljava/lang/String;", false); 93 | } else { 94 | mv.visitMethodInsn(INVOKESTATIC, "w/Global", "toJson", "(Ljava/lang/Object;)Ljava/lang/String;", false); 95 | } 96 | int paramsVarIndex = newLocal(Type.getType(String.class)); 97 | mv.visitVarInsn(ASTORE, paramsVarIndex); 98 | return paramsVarIndex; 99 | } 100 | 101 | /** 102 | * return value toString and store in local variable, return the local variable index 103 | * 104 | * It's very useful for enhancement like watch out-watch. 105 | * @param mv 106 | * @param descriptor 107 | * @return 108 | */ 109 | protected int asmStoreRetString(MethodVisitor mv, String descriptor, int printFormat) { 110 | int returnValueVarIndex = newLocal(Type.getType(String.class)); 111 | return asmStoreRetString(mv, descriptor, printFormat, returnValueVarIndex); 112 | } 113 | 114 | /** 115 | * return value toString and store in local variable 116 | * @param mv 117 | * @param descriptor 118 | * @param printFormat 119 | * @param returnValueVarIndex given local variable index 120 | * @return 121 | */ 122 | protected int asmStoreRetString(MethodVisitor mv, String descriptor, int printFormat, int returnValueVarIndex) { 123 | Type returnType = Type.getReturnType(descriptor); 124 | switch (returnType.getSort()) { 125 | case Type.DOUBLE: 126 | case Type.LONG: 127 | mv.visitInsn(DUP2); 128 | box(returnType); 129 | formatResult(printFormat); 130 | break; 131 | case Type.BOOLEAN: 132 | case Type.CHAR: 133 | case Type.INT: 134 | case Type.FLOAT: 135 | case Type.SHORT: 136 | case Type.BYTE: 137 | mv.visitInsn(DUP); 138 | box(returnType); 139 | formatResult(printFormat); 140 | break; 141 | case Type.ARRAY: 142 | case Type.OBJECT: 143 | mv.visitInsn(DUP); 144 | formatResult(printFormat); 145 | break; 146 | case Type.VOID: 147 | default: 148 | mv.visitInsn(Opcodes.ACONST_NULL); 149 | } 150 | mv.visitVarInsn(ASTORE, returnValueVarIndex); 151 | return returnValueVarIndex; 152 | } 153 | 154 | private void formatResult(int printFormat) { 155 | if (printFormat == 1) { 156 | mv.visitMethodInsn(INVOKESTATIC, "java/lang/String", "valueOf", "(Ljava/lang/Object;)Ljava/lang/String;", false); 157 | } else { 158 | mv.visitMethodInsn(INVOKESTATIC, "w/Global", "toJson", "(Ljava/lang/Object;)Ljava/lang/String;", false); 159 | } 160 | } 161 | 162 | /** 163 | * similar process with loadArgArray, but for sub method params 164 | * @param descriptor 165 | * @return 166 | */ 167 | private int subCallParamsToArray(String descriptor) { 168 | Type[] argumentTypes = Type.getArgumentTypes(descriptor); 169 | int[] loads = new int[argumentTypes.length]; 170 | int[] index = new int[argumentTypes.length]; 171 | for (int i = argumentTypes.length - 1; i >= 0; i--) { 172 | switch (argumentTypes[i].getSort()) { 173 | case Type.LONG: 174 | int li = newLocal(Type.LONG_TYPE); 175 | mv.visitVarInsn(LSTORE, li); 176 | index[i] = li; loads[i] = LLOAD; 177 | break; 178 | case Type.DOUBLE: 179 | int di = newLocal(Type.DOUBLE_TYPE); 180 | mv.visitVarInsn(DSTORE, di); 181 | index[i] = di;loads[i] = DLOAD; 182 | break; 183 | case Type.BOOLEAN: 184 | int zi = newLocal(Type.BOOLEAN_TYPE); 185 | mv.visitVarInsn(ISTORE, zi); 186 | index[i] = zi;loads[i] = ILOAD; 187 | break; 188 | case Type.BYTE: 189 | int bi = newLocal(Type.BYTE_TYPE); 190 | mv.visitVarInsn(ISTORE, bi); 191 | index[i] = bi;loads[i] = ILOAD; 192 | break; 193 | case Type.CHAR: 194 | int ci = newLocal(Type.CHAR_TYPE); 195 | mv.visitVarInsn(ISTORE, ci); 196 | index[i] = ci;loads[i] = ILOAD; 197 | break; 198 | case Type.SHORT: 199 | int si = newLocal(Type.SHORT_TYPE); 200 | mv.visitVarInsn(ISTORE, si); 201 | index[i] = si;loads[i] = ILOAD; 202 | break; 203 | case Type.FLOAT: 204 | int fi = newLocal(Type.FLOAT_TYPE); 205 | mv.visitVarInsn(FSTORE, fi); 206 | index[i] = fi;loads[i] = FLOAD; 207 | break; 208 | case Type.INT: 209 | int ii = newLocal(Type.INT_TYPE); 210 | mv.visitVarInsn(ISTORE, ii); 211 | index[i] = ii;loads[i] = ILOAD; 212 | break; 213 | default: 214 | int ai = newLocal(Type.getType(Object.class)); 215 | mv.visitVarInsn(ASTORE, ai); 216 | index[i] = ai;loads[i] = ALOAD; 217 | break; 218 | } 219 | } 220 | push(argumentTypes.length); 221 | newArray(Type.getObjectType("java/lang/Object")); 222 | for (int i = 0; i < index.length; i++) { 223 | dup(); 224 | push(i); 225 | mv.visitVarInsn(loads[i], index[i]); 226 | box(argumentTypes[i]); 227 | arrayStore(Type.getObjectType("java/lang/Object")); 228 | } 229 | int result = newLocal(Type.getType(Object.class)); 230 | mv.visitVarInsn(ASTORE, result); 231 | for (int i = 0; i < index.length; i++) { 232 | mv.visitVarInsn(loads[i], index[i]); 233 | } 234 | return result; 235 | } 236 | 237 | /** 238 | * generate StringBuilder and append method, after method, the stringBuilder address will at the top of stack 239 | * @param mv 240 | * @param list 241 | */ 242 | protected void asmGenerateStringBuilder(MethodVisitor mv, List list) { 243 | if (list == null || list.isEmpty()) { 244 | return; 245 | } 246 | mv.visitTypeInsn(NEW, "java/lang/StringBuilder"); 247 | mv.visitInsn(DUP); 248 | mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "", "()V", false); 249 | 250 | for (SbNode subStringNode : list) { 251 | subStringNode.loadAndAppend(mv); 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/main/java/w/core/compiler/WCompiler.java: -------------------------------------------------------------------------------- 1 | package w.core.compiler; 2 | 3 | import org.benf.cfr.reader.api.CfrDriver; 4 | import org.benf.cfr.reader.api.OutputSinkFactory; 5 | import org.benf.cfr.reader.bytecode.analysis.parse.utils.Pair; 6 | import org.benf.cfr.reader.state.ClassFileSourceImpl; 7 | import org.codehaus.commons.compiler.CompileException; 8 | import org.codehaus.janino.SimpleCompiler; 9 | import w.Global; 10 | 11 | import java.io.IOException; 12 | import java.util.ArrayList; 13 | import java.util.Collection; 14 | import java.util.HashMap; 15 | import java.util.List; 16 | 17 | /** 18 | * @author Frank 19 | * @date 2024/6/26 0:00 20 | */ 21 | public class WCompiler { 22 | 23 | // generate a compiler every time to avoid conflict 24 | private static SimpleCompiler getCompiler() { 25 | SimpleCompiler compiler = new SimpleCompiler(); 26 | compiler.setParentClassLoader(Global.getClassLoader()); 27 | return compiler; 28 | } 29 | 30 | /** 31 | * Compile a class 32 | * @param content the class content, must have only one class declaration. 33 | * @return 34 | * @throws CompileException 35 | */ 36 | public static byte[] compileWholeClass(String content) throws CompileException { 37 | SimpleCompiler compiler = getCompiler(); 38 | compiler.cook(content); 39 | // only one class will be compiled 40 | String className = compiler.getBytecodes().keySet().iterator().next(); 41 | return compiler.getBytecodes().get(className); 42 | } 43 | 44 | /** 45 | * Compile a method 46 | * @param className the wrapper class name 47 | * @param methodContent the method content, like public void foo(){ ...} 48 | * @return 49 | * @throws CompileException 50 | */ 51 | public static byte[] compileMethod(String className, String methodContent) throws CompileException { 52 | String packageName = className.substring(0, className.lastIndexOf(".")); 53 | String simpleClassName = className.substring(className.lastIndexOf(".") +1); 54 | return compileWholeClass("package " + packageName +";\n import java.util.*;\n public class " + simpleClassName + " {" + methodContent + "}"); 55 | } 56 | 57 | /** 58 | * Compile a method, wrapped in a Dynamic class. 59 | * @param content { some code; } 60 | * @return 61 | * @throws CompileException 62 | */ 63 | public static byte[] compileDynamicCodeBlock(String reType, String content) throws CompileException { 64 | return compileMethod("w.Dynamic", "public "+ reType +" replace()" + content); 65 | } 66 | 67 | /** 68 | * Decompile a class 69 | * @param byteCode 70 | * @return 71 | */ 72 | public static String decompile(byte[] byteCode) { 73 | StringBuilder sb = new StringBuilder(); 74 | OutputSinkFactory outputSinkFactory = new OutputSinkFactory() { 75 | @Override 76 | public List getSupportedSinks(SinkType sinkType, Collection collection) { 77 | return new ArrayList() { { add(SinkClass.STRING); }}; 78 | } 79 | @Override 80 | public Sink getSink(SinkType sinkType, SinkClass sinkClass) { 81 | return sinkable -> sb.append(sinkable.toString()).append('\n'); 82 | } 83 | }; 84 | CfrDriver driver = new CfrDriver.Builder() 85 | // fix: 中文显示为Unicode 86 | .withOptions(new HashMap() {{ 87 | put("hideutf", "false"); 88 | }}) 89 | .withClassFileSource(new ClassFileSourceImpl(null) { 90 | @Override 91 | public Pair getClassFileContent(String path) throws IOException { 92 | if (path.equals("tmp.class")) { 93 | return Pair.make(byteCode, path); 94 | } 95 | return null; 96 | } 97 | }) 98 | .withOutputSink(outputSinkFactory).build(); 99 | List tmp = new ArrayList<>(); 100 | tmp.add("tmp.class"); 101 | driver.analyse(tmp); 102 | String res = sb.toString(); 103 | return res.substring(!res.contains(" */\n") ? 0 : res.indexOf(" */\n") + 3); 104 | } 105 | } -------------------------------------------------------------------------------- /src/main/java/w/core/constant/Codes.java: -------------------------------------------------------------------------------- 1 | package w.core.constant; 2 | 3 | /** 4 | * @author Frank 5 | * @date 2024/6/23 16:17 6 | */ 7 | public class Codes { 8 | public static int printFormatForToString = 1; 9 | 10 | public static int printFormatForToJson = 2; 11 | 12 | public static int changeBodyModeUseJavassist = 0; 13 | 14 | public static int changeBodyModeUseASM = 1; 15 | 16 | public static int changeResultModeUseJavassist = 0; 17 | 18 | public static int changeResultModeUseASM = 1; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/w/core/model/BaseClassTransformer.java: -------------------------------------------------------------------------------- 1 | package w.core.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.objectweb.asm.Type; 6 | import org.objectweb.asm.tree.AbstractInsnNode; 7 | import org.objectweb.asm.tree.InsnNode; 8 | import org.objectweb.asm.tree.VarInsnNode; 9 | import w.Global; 10 | import w.util.RequestUtils; 11 | 12 | import java.io.IOException; 13 | import java.lang.instrument.ClassFileTransformer; 14 | import java.lang.instrument.IllegalClassFormatException; 15 | import java.security.ProtectionDomain; 16 | import java.util.*; 17 | import java.util.concurrent.CompletableFuture; 18 | 19 | import static org.objectweb.asm.Opcodes.*; 20 | 21 | /** 22 | * @author Frank 23 | * @date 2023/12/21 23:45 24 | */ 25 | @Getter 26 | @Setter 27 | public abstract class BaseClassTransformer implements ClassFileTransformer { 28 | protected UUID uuid = UUID.randomUUID(); 29 | 30 | protected String className; 31 | 32 | protected String traceId; 33 | 34 | protected int status; 35 | 36 | 37 | 38 | public abstract byte[] transform(byte[] origin) throws Exception; 39 | 40 | public abstract String desc(); 41 | 42 | @Override 43 | public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] origin) throws IllegalClassFormatException { 44 | if (className == null) return origin; 45 | className = className.replace("/", "."); 46 | if (Objects.equals(this.className, className)) { 47 | try{ 48 | byte[] r = transform(origin); 49 | Global.info(className + " transformer " + uuid + " added success <(^-^)>"); 50 | return r; 51 | } catch (Exception e) { 52 | Global.error(className + " transformer " + uuid + " added fail -(′д`)-: ", e); 53 | // async to delete, because current thread holds the class lock 54 | CompletableFuture.runAsync(() -> Global.deleteTransformer(uuid)); 55 | } 56 | } 57 | return null; 58 | } 59 | 60 | public void clear() { 61 | 62 | } 63 | protected String paramTypesToDescriptor(List paramTypes) { 64 | StringBuilder s = new StringBuilder(); 65 | for (String paramType : paramTypes) { 66 | s.append(paramTypeToDescriptor(paramType)); 67 | } 68 | return "(" + s + ")"; 69 | } 70 | 71 | protected String paramTypeToDescriptor(String paramType) { 72 | if (paramType == null || paramType.isEmpty() || paramType.contains("<")) { 73 | throw new IllegalArgumentException("error type"); 74 | } 75 | switch (paramType) { 76 | case "int": 77 | return "I"; 78 | case "long": 79 | return "J"; 80 | case "float": 81 | return "F"; 82 | case "boolean": 83 | return "Z"; 84 | case "double": 85 | return "D"; 86 | case "byte": 87 | return "B"; 88 | case "short": 89 | return "S"; 90 | case "char": 91 | return "C"; 92 | default: 93 | if (paramType.endsWith("[]")) { 94 | return "[" + paramTypeToDescriptor(paramType.substring(0, paramType.length() - 2)); 95 | } 96 | return "L" + paramType.replace(".", "/") + ";"; 97 | } 98 | } 99 | 100 | protected AbstractInsnNode loadVar(Type type, int index) { 101 | switch (type.getSort()) { 102 | case Type.INT: 103 | case Type.SHORT: 104 | case Type.BYTE: 105 | case Type.BOOLEAN: 106 | case Type.CHAR: 107 | return new VarInsnNode(ILOAD, index); 108 | case Type.FLOAT: 109 | return new VarInsnNode(FLOAD, index); 110 | case Type.DOUBLE: 111 | return new VarInsnNode(DLOAD, index); 112 | case Type.LONG: 113 | return new VarInsnNode(LLOAD, index); 114 | case Type.ARRAY: 115 | case Type.OBJECT: 116 | return new VarInsnNode(ALOAD, index); 117 | case Type.VOID: 118 | return new InsnNode(NOP); 119 | default: 120 | throw new RuntimeException("Unsupport type"); 121 | } 122 | } 123 | 124 | protected List storeVarWithDefaultValue(Type type, int index) { 125 | List result = new ArrayList<>(); 126 | switch (type.getSort()) { 127 | case Type.INT: 128 | case Type.SHORT: 129 | case Type.BYTE: 130 | case Type.BOOLEAN: 131 | case Type.CHAR: 132 | result.add(new InsnNode(ICONST_0)); 133 | result.add(new VarInsnNode(ISTORE, index)); 134 | return result; 135 | case Type.FLOAT: 136 | result.add(new InsnNode(FCONST_0)); 137 | result.add(new VarInsnNode(FSTORE, index)); 138 | return result; 139 | case Type.DOUBLE: 140 | result.add(new InsnNode(DCONST_0)); 141 | result.add(new VarInsnNode(DSTORE, index)); 142 | return result; 143 | case Type.LONG: 144 | result.add(new InsnNode(LCONST_0)); 145 | result.add(new VarInsnNode(LSTORE, index)); 146 | return result; 147 | case Type.ARRAY: 148 | case Type.OBJECT: 149 | result.add(new InsnNode(ACONST_NULL)); 150 | result.add(new VarInsnNode(ASTORE, index)); 151 | return result; 152 | case Type.VOID: 153 | return result; 154 | default: 155 | throw new RuntimeException("Unsupport type"); 156 | } 157 | } 158 | } -------------------------------------------------------------------------------- /src/main/java/w/core/model/ChangeBodyTransformer.java: -------------------------------------------------------------------------------- 1 | package w.core.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import javassist.CtClass; 5 | import javassist.CtMethod; 6 | import javassist.Modifier; 7 | import lombok.Data; 8 | 9 | import org.codehaus.commons.compiler.CompileException; 10 | import org.objectweb.asm.*; 11 | import org.objectweb.asm.tree.*; 12 | import w.Global; 13 | import w.core.compiler.WCompiler; 14 | import w.core.constant.Codes; 15 | import w.web.message.ChangeBodyMessage; 16 | 17 | import java.io.ByteArrayInputStream; 18 | import java.util.Arrays; 19 | import java.util.List; 20 | import java.util.Objects; 21 | 22 | /** 23 | * @author Frank 24 | * @date 2023/12/21 13:46 25 | */ 26 | @Data 27 | public class ChangeBodyTransformer extends BaseClassTransformer { 28 | 29 | @JsonIgnore 30 | transient ChangeBodyMessage message; 31 | 32 | String method; 33 | 34 | List paramTypes; 35 | 36 | int mode; 37 | 38 | public ChangeBodyTransformer(ChangeBodyMessage message) { 39 | this.className = message.getClassName(); 40 | this.method = message.getMethod(); 41 | this.message = message; 42 | this.traceId = message.getId(); 43 | this.paramTypes = message.getParamTypes(); 44 | this.mode = message.getMode(); 45 | } 46 | 47 | @Override 48 | public byte[] transform(byte[] origin) throws Exception { 49 | byte[] result = null; 50 | if (mode == Codes.changeBodyModeUseJavassist) { 51 | // use javassist, message.body is the method body, a code block starts with { ends with } 52 | result = changeBodyByJavassist(origin); 53 | } else if (mode == Codes.changeBodyModeUseASM) { 54 | // use asm, message.body is the whole method including signature, like `public void hi {}` 55 | result = changeBodyByASM(origin); 56 | } 57 | status = 1; 58 | return result; 59 | } 60 | 61 | private byte[] changeBodyByJavassist(byte[] origin) throws Exception { 62 | CtClass ctClass = Global.classPool.makeClass(new ByteArrayInputStream(origin)); 63 | boolean effect = false; 64 | for (CtMethod declaredMethod : ctClass.getDeclaredMethods()) { 65 | if (Objects.equals(declaredMethod.getName(), method) && 66 | Arrays.equals(paramTypes.toArray(new String[0]), 67 | Arrays.stream(declaredMethod.getParameterTypes()).map(CtClass::getName).toArray()) 68 | ) { 69 | if ((declaredMethod.getModifiers() & Modifier.ABSTRACT) != 0) { 70 | throw new IllegalArgumentException("Cannot change abstract method."); 71 | } 72 | if ((declaredMethod.getModifiers() & Modifier.NATIVE) != 0) { 73 | throw new IllegalArgumentException("Cannot change native method."); 74 | } 75 | declaredMethod.setBody(message.getBody()); 76 | effect = true; 77 | } 78 | } 79 | if (!effect) { 80 | throw new IllegalArgumentException("Method not declared here."); 81 | } 82 | byte[] result = ctClass.toBytecode(); 83 | ctClass.detach(); 84 | return result; 85 | } 86 | 87 | private byte[] changeBodyByASM(byte[] origin) throws Exception { 88 | 89 | boolean effect = false; 90 | String paramDes = paramTypesToDescriptor(paramTypes); 91 | 92 | ClassReader cr = new ClassReader(origin); 93 | ClassReader rcr = null; 94 | 95 | ClassNode targetClassNode = new ClassNode(); 96 | cr.accept(targetClassNode, ClassReader.EXPAND_FRAMES); 97 | 98 | for (MethodNode mn : targetClassNode.methods) { 99 | // find target method 100 | if (mn.name.equals(method) && mn.desc.startsWith(paramDes)) { 101 | // compile replacement 102 | rcr = compileReplacement(mn); 103 | ClassNode replacementClassNode = new ClassNode(); 104 | rcr.accept(replacementClassNode, 0); 105 | for (MethodNode rmn : replacementClassNode.methods) { 106 | if (rmn.name.equals(method) && rmn.desc.startsWith(paramDes)) { 107 | mn.instructions = rmn.instructions; 108 | mn.tryCatchBlocks = rmn.tryCatchBlocks; 109 | mn.localVariables = rmn.localVariables; 110 | effect = true; 111 | break; 112 | } 113 | } 114 | } 115 | } 116 | if (!effect) { 117 | throw new IllegalArgumentException("Method not declared here."); 118 | } 119 | ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS) { 120 | @Override 121 | protected ClassLoader getClassLoader() { 122 | return Global.getClassLoader(); 123 | } 124 | }; 125 | 126 | targetClassNode.accept(classWriter); 127 | byte[] result = classWriter.toByteArray(); 128 | return result; 129 | } 130 | 131 | public boolean equals(Object other) { 132 | if (other instanceof ChangeBodyTransformer) { 133 | return this.uuid.equals(((ChangeBodyTransformer) other).getUuid()); 134 | } 135 | return false; 136 | } 137 | 138 | @Override 139 | public String desc() { 140 | return "ChangeBody_" + getClassName() + "#" + method + " " + paramTypes; 141 | } 142 | 143 | private ClassReader compileReplacement(MethodNode mn) { 144 | try { 145 | String descriptor = mn.desc; 146 | List exceptions = mn.exceptions; 147 | StringBuilder m = new StringBuilder(); 148 | m.append(Type.getReturnType(descriptor).getClassName()).append(" ").append(method).append("("); 149 | Type[] params = Type.getArgumentTypes(descriptor); 150 | for (int i = 0; i < params.length; i++) { 151 | if (i != 0) m.append(", "); 152 | m.append(params[i].getClassName()).append(" ").append("$").append(i + 1); 153 | } 154 | m.append(")"); 155 | 156 | if (exceptions != null && !exceptions.isEmpty()) { 157 | m.append("throws "); 158 | for (int i = 0; i < exceptions.size(); i++) { 159 | if (i != 0) m.append(","); 160 | m.append(exceptions.get(i).replace("/", ".")); 161 | } 162 | } 163 | 164 | m.append(message.getBody()); 165 | 166 | return new ClassReader(WCompiler.compileMethod(className, m.toString())); 167 | } catch (CompileException e) { 168 | throw new IllegalArgumentException("Source code compile error", e); 169 | } 170 | } 171 | 172 | } -------------------------------------------------------------------------------- /src/main/java/w/core/model/DecompileTransformer.java: -------------------------------------------------------------------------------- 1 | package w.core.model; 2 | 3 | import javassist.CannotCompileException; 4 | import javassist.CtClass; 5 | import javassist.CtMethod; 6 | import javassist.NotFoundException; 7 | import lombok.Data; 8 | import org.objectweb.asm.*; 9 | import w.Global; 10 | import w.core.asm.WAdviceAdapter; 11 | import w.core.compiler.WCompiler; 12 | import w.web.message.DecompileMessage; 13 | import w.web.message.WatchMessage; 14 | 15 | import java.io.FileOutputStream; 16 | import java.util.concurrent.CompletableFuture; 17 | import java.util.concurrent.atomic.AtomicBoolean; 18 | 19 | import static org.objectweb.asm.Opcodes.ASM9; 20 | 21 | 22 | /** 23 | * @author Frank 24 | * @date 2023/12/21 13:46 25 | */ 26 | @Data 27 | public class DecompileTransformer extends BaseClassTransformer { 28 | 29 | transient DecompileMessage message; 30 | 31 | public DecompileTransformer(DecompileMessage decompileMessage) { 32 | this.className = decompileMessage.getClassName(); 33 | this.message = decompileMessage; 34 | this.traceId = decompileMessage.getId(); 35 | } 36 | 37 | @Override 38 | public byte[] transform(byte[] origin) throws Exception { 39 | String sourceCode = WCompiler.decompile(origin); 40 | Global.info("/* " + className + " source code: */\n" + sourceCode); 41 | CompletableFuture.runAsync(() -> Global.deleteTransformer(uuid)); 42 | return origin; 43 | } 44 | 45 | public boolean equals(Object other) { 46 | if (other instanceof DecompileTransformer) { 47 | return this.uuid.equals(((DecompileTransformer) other).getUuid()); 48 | } 49 | return false; 50 | } 51 | 52 | @Override 53 | public String desc() { 54 | return "Decompile_" + getClassName(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/w/core/model/OuterWatchTransformer.java: -------------------------------------------------------------------------------- 1 | package w.core.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import javassist.*; 5 | import javassist.expr.ExprEditor; 6 | import javassist.expr.MethodCall; 7 | import lombok.Data; 8 | import org.objectweb.asm.*; 9 | import w.Global; 10 | import w.core.asm.SbNode; 11 | import w.core.asm.WAdviceAdapter; 12 | import w.web.message.OuterWatchMessage; 13 | 14 | import java.io.FileOutputStream; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | import static org.objectweb.asm.Opcodes.*; 19 | 20 | 21 | /** 22 | * @author Frank 23 | * @date 2023/12/21 13:46 24 | */ 25 | @Data 26 | public class OuterWatchTransformer extends BaseClassTransformer { 27 | 28 | @JsonIgnore 29 | transient OuterWatchMessage message; 30 | 31 | String method; 32 | 33 | String innerClassName; 34 | 35 | String innerMethod; 36 | 37 | int printFormat; 38 | 39 | 40 | public OuterWatchTransformer(OuterWatchMessage watchMessage) { 41 | this.message = watchMessage; 42 | this.className = watchMessage.getSignature().split("#")[0]; 43 | this.method = watchMessage.getSignature().split("#")[1]; 44 | this.innerClassName = watchMessage.getInnerSignature().split("#")[0]; 45 | this.innerMethod = watchMessage.getInnerSignature().split("#")[1]; 46 | this.printFormat = watchMessage.getPrintFormat(); 47 | this.traceId = watchMessage.getId(); 48 | } 49 | 50 | @Override 51 | public byte[] transform(byte[] origin) throws Exception { 52 | ClassReader classReader = new ClassReader(origin); 53 | ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES) { 54 | @Override 55 | protected ClassLoader getClassLoader() { 56 | return Global.getClassLoader(); 57 | } 58 | }; 59 | 60 | classReader.accept(new ClassVisitor(ASM9, classWriter) { 61 | @Override 62 | public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { 63 | MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); 64 | if (!name.equals(method)) return mv; 65 | return new WAdviceAdapter(ASM9, mv, access, name, descriptor) { 66 | private int line; 67 | private int startTimeVarIndex; 68 | 69 | private int paramsVarIndex; 70 | 71 | private int returnValueVarIndex; 72 | 73 | private int exceptionStringIndex; 74 | 75 | @Override 76 | public void visitLineNumber(int line, Label start) { 77 | super.visitLineNumber(line, start); 78 | this.line = line; 79 | } 80 | 81 | @Override 82 | public void visitMethodInsn(int opcodeAndSource, String owner, String name, String descriptor, boolean isInterface) { 83 | boolean hit = (owner.replace("/", ".").equals(innerClassName) || "*".equals(innerClassName)) 84 | && name.equals(innerMethod); 85 | if (hit) { 86 | // long start = System.currentTimeMillis(); 87 | startTimeVarIndex = asmStoreStartTime(mv); 88 | // String params = Arrays.toString(paramArray); 89 | paramsVarIndex = asmSubCallStoreParamsString(mv, printFormat, descriptor); 90 | 91 | mv.visitLdcInsn(traceId); 92 | mv.visitMethodInsn(INVOKESTATIC, "w/util/RequestUtils", "fillCurThread", "(Ljava/lang/String;)V", false); 93 | 94 | 95 | Label tryStart = new Label(); 96 | Label tryEnd = new Label(); 97 | Label catchStart = new Label(); 98 | mv.visitTryCatchBlock(tryStart, tryEnd, catchStart, "java/lang/Throwable"); 99 | mv.visitLabel(tryStart); 100 | // execute original method 101 | mv.visitMethodInsn(opcodeAndSource, owner, name, descriptor, isInterface); 102 | 103 | returnValueVarIndex = asmStoreRetString(mv, descriptor, printFormat); 104 | 105 | // long duration = System.currentTimeMillis() - start; 106 | int durationVarIndex = asmCalculateCost(mv, startTimeVarIndex); 107 | 108 | // return value duplication 109 | int returnValueVarIndex = asmStoreRetString(mv, descriptor, printFormat); 110 | // new StringBuilder().append("line:" + line + ", request: ").append(params).append(", response: ").append(returnValue).append(", cost: ").append(duration).append("ms"); 111 | List list = new ArrayList(); 112 | list.add(new SbNode("line:" + line + ", req: ")); 113 | list.add(new SbNode(ALOAD, paramsVarIndex)); 114 | list.add(new SbNode(", response: ")); 115 | list.add(new SbNode(ALOAD, returnValueVarIndex)); 116 | list.add(new SbNode(", cost: ")); 117 | list.add(new SbNode(LLOAD, durationVarIndex)); 118 | list.add(new SbNode("ms")); 119 | asmGenerateStringBuilder(mv, list); 120 | 121 | /*---------------------counter: if reach the limitation will remove the transformer----------------*/ 122 | mv.visitLdcInsn(uuid.toString()); 123 | mv.visitMethodInsn(INVOKESTATIC, "w/Global", "checkCountAndUnload", "(Ljava/lang/String;)V", false); 124 | 125 | // info the string builder 126 | mv.visitMethodInsn(INVOKESTATIC, "w/Global", "info", "(Ljava/lang/Object;)V", false); 127 | 128 | 129 | mv.visitLabel(tryEnd); 130 | Label end = new Label(); 131 | mv.visitJumpInsn(Opcodes.GOTO, end); 132 | 133 | mv.visitLabel(catchStart); 134 | int exceptionIndex = newLocal(Type.getType(Throwable.class)); 135 | mv.visitVarInsn(Opcodes.ASTORE, exceptionIndex); 136 | mv.visitVarInsn(Opcodes.ALOAD, exceptionIndex); 137 | exceptionStringIndex = newLocal(Type.getType(String.class)); 138 | mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "toString", "()Ljava/lang/String;", false); 139 | mv.visitVarInsn(Opcodes.ASTORE, exceptionStringIndex); 140 | 141 | postProcess(true); 142 | mv.visitVarInsn(Opcodes.ALOAD, exceptionIndex); 143 | mv.visitInsn(Opcodes.ATHROW); 144 | Label catchEnd = new Label(); 145 | mv.visitLabel(catchEnd); 146 | mv.visitLabel(end); 147 | 148 | mv.visitMethodInsn(INVOKESTATIC, "w/util/RequestUtils", "clearRequestCtx", "()V", false); 149 | } else { 150 | mv.visitMethodInsn(opcodeAndSource, owner, name, descriptor, isInterface); 151 | } 152 | } 153 | 154 | private void postProcess(boolean whenThrow) { 155 | push(line); 156 | loadLocal(startTimeVarIndex, Type.LONG_TYPE); 157 | push(uuid.toString()); 158 | push(traceId); 159 | push(innerClassName.substring(innerClassName.lastIndexOf('.') + 1) + "#" + innerMethod); 160 | loadLocal(paramsVarIndex, Type.getType(String.class)); 161 | 162 | if (whenThrow) { 163 | mv.visitInsn(Opcodes.ACONST_NULL); 164 | mv.visitVarInsn(ALOAD, exceptionStringIndex); 165 | } else { 166 | mv.visitVarInsn(ALOAD, returnValueVarIndex); 167 | mv.visitInsn(Opcodes.ACONST_NULL); 168 | } 169 | 170 | mv.visitMethodInsn(INVOKESTATIC, "w/core/asm/Tool", "outerWatchPostProcess", "(IJLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", false); 171 | } 172 | 173 | }; 174 | } 175 | }, ClassReader.EXPAND_FRAMES); 176 | byte[] result = classWriter.toByteArray(); 177 | status = 1; 178 | return result; 179 | } 180 | 181 | public boolean equals(Object other) { 182 | if (other instanceof OuterWatchTransformer) { 183 | return this.uuid.equals(((OuterWatchTransformer) other).getUuid()); 184 | } 185 | return false; 186 | } 187 | 188 | @Override 189 | public String desc() { 190 | return "OuterWatch_" + getClassName() + "#" + method; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/main/java/w/core/model/ReplaceClassTransformer.java: -------------------------------------------------------------------------------- 1 | package w.core.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 5 | import lombok.Data; 6 | import w.Global; 7 | import w.web.message.ReplaceClassMessage; 8 | 9 | import java.io.IOException; 10 | import java.util.Base64; 11 | 12 | /** 13 | * @author Frank 14 | * @date 2023/12/21 13:46 15 | */ 16 | @Data 17 | public class ReplaceClassTransformer extends BaseClassTransformer { 18 | 19 | @JsonIgnore 20 | transient ReplaceClassMessage message; 21 | 22 | byte[] content; 23 | 24 | public ReplaceClassTransformer(ReplaceClassMessage message) throws IOException { 25 | this.className = message.getClassName(); 26 | this.message = message; 27 | this.content = Base64.getDecoder().decode(message.getContent());; 28 | this.traceId = message.getId(); 29 | } 30 | 31 | @Override 32 | public byte[] transform(byte[] origin) throws Exception { 33 | status = 1; 34 | return content; 35 | } 36 | 37 | public boolean equals(Object other) { 38 | if (other instanceof ReplaceClassTransformer) { 39 | return this.uuid.equals(((ReplaceClassTransformer) other).getUuid()); 40 | } 41 | return false; 42 | } 43 | @Override 44 | public String desc() { 45 | return "ReplaceClass_" + getClassName(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/w/core/model/TraceTransformer.java: -------------------------------------------------------------------------------- 1 | package w.core.model; 2 | 3 | import java.util.*; 4 | import java.util.concurrent.atomic.AtomicBoolean; 5 | 6 | import lombok.Data; 7 | import org.objectweb.asm.*; 8 | import w.Global; 9 | import w.core.asm.WAdviceAdapter; 10 | import w.web.message.TraceMessage; 11 | 12 | import static org.objectweb.asm.Opcodes.ASM9; 13 | 14 | @Data 15 | public class TraceTransformer extends BaseClassTransformer { 16 | 17 | public static ThreadLocal> traceCtx = ThreadLocal.withInitial(HashMap::new); 18 | 19 | transient TraceMessage message; 20 | 21 | String method; 22 | 23 | int minCost; 24 | 25 | boolean ignoreZero; 26 | 27 | public TraceTransformer(TraceMessage traceMessage) { 28 | this.message = traceMessage; 29 | this.className = traceMessage.getSignature().split("#")[0]; 30 | this.method = traceMessage.getSignature().split("#")[1]; 31 | this.traceId = traceMessage.getId(); 32 | this.minCost = traceMessage.getMinCost(); 33 | this.ignoreZero = traceMessage.isIgnoreZero(); 34 | } 35 | 36 | @Override 37 | public byte[] transform(byte[] origin) throws Exception { 38 | ClassReader classReader = new ClassReader(origin); 39 | ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES) { 40 | @Override 41 | protected ClassLoader getClassLoader() { 42 | return Global.getClassLoader(); 43 | } 44 | }; 45 | AtomicBoolean effect = new AtomicBoolean(); 46 | classReader.accept(new ClassVisitor(ASM9, classWriter) { 47 | @Override 48 | public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { 49 | MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); 50 | if (!name.equals(method)) return mv; 51 | effect.set(true); 52 | return new WAdviceAdapter(ASM9, mv, access, name, descriptor) { 53 | private int startTimeVarIndex; 54 | 55 | private int line; 56 | @Override 57 | public void visitLineNumber(int line, Label start) { 58 | super.visitLineNumber(line, start); 59 | this.line = line; 60 | } 61 | 62 | @Override 63 | public void onMethodEnter(){ 64 | startTimeVarIndex = asmStoreStartTime(mv); 65 | } 66 | @Override 67 | protected void onMethodExit(int opcode) { 68 | mv.visitVarInsn(LLOAD, startTimeVarIndex); 69 | mv.visitLdcInsn((long) minCost); 70 | mv.visitLdcInsn(uuid.toString()); 71 | mv.visitLdcInsn(traceId); 72 | mv.visitLdcInsn(className + "#" + method); 73 | mv.visitLdcInsn(ignoreZero); 74 | mv.visitMethodInsn(INVOKESTATIC, "w/core/model/TraceTransformer", "traceSummary", "(JJLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V", false); 75 | } 76 | 77 | @Override 78 | public void visitMethodInsn(int opcodeAndSource, String owner, String name, String descriptor, boolean isInterface) { 79 | // by default ignore the , append, toString methods 80 | if (Global.ignoreTraceMethods.contains(name) || 81 | (owner.startsWith("java/lang") && !owner.startsWith("java/lang/Thread"))) { 82 | super.visitMethodInsn(opcodeAndSource, owner, name, descriptor, isInterface); 83 | return; 84 | } 85 | 86 | if (owner.replace("/",".").equals(className) && method.equals(name)) { 87 | mv.visitLdcInsn(uuid.toString()); 88 | mv.visitMethodInsn(INVOKESTATIC, "w/core/model/TraceTransformer", "recursiveRecord", "(Ljava/lang/String;)V", false); 89 | } 90 | 91 | // long start = System.currentTimeMillis(); 92 | mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false); 93 | int localStart = newLocal(Type.LONG_TYPE); 94 | mv.visitVarInsn(LSTORE, localStart); 95 | 96 | // execute original method 97 | super.visitMethodInsn(opcodeAndSource, owner, name, descriptor, isInterface); 98 | // long duration = System.currentTimeMillis() - start; 99 | int localDurationIndex = asmCalculateCost(mv, localStart); 100 | mv.visitLdcInsn(uuid.toString()); 101 | mv.visitLdcInsn("line" + line + "," + owner.replace("/", ".") + "#" + name); 102 | mv.visitVarInsn(LLOAD, localDurationIndex); 103 | mv.visitMethodInsn(INVOKESTATIC, "w/core/model/TraceTransformer", "subTrace", "(Ljava/lang/String;Ljava/lang/String;J)V", false); 104 | } 105 | }; 106 | } 107 | }, ClassReader.EXPAND_FRAMES); 108 | if (!effect.get()) { 109 | throw new IllegalArgumentException("Method not declared here."); 110 | } 111 | byte[] result = classWriter.toByteArray(); 112 | status = 1; 113 | return result; 114 | } 115 | 116 | public static void subTrace(String uuid, String key, long duration) { 117 | Map map = traceCtx.get().computeIfAbsent(uuid, k->new TraceCtx()).traceContent; 118 | int[] arr = map.computeIfAbsent(key, k->new int[2]); 119 | arr[0] += (int) duration; 120 | arr[1] += 1; 121 | } 122 | 123 | 124 | public static void traceSummary(long start, long minCost, String uuid, String traceId, String outerSig, boolean ignoreZero) { 125 | TraceCtx ctx = traceCtx.get().getOrDefault(uuid, new TraceCtx()); 126 | int deep = --ctx.stackDeep; 127 | StringBuilder sb = new StringBuilder(); 128 | if (deep <= 0) { 129 | long cost = System.currentTimeMillis() - start; 130 | if (cost >= minCost) { 131 | w.Global.checkCountAndUnload(uuid); 132 | w.util.RequestUtils.fillCurThread(traceId); 133 | sb.append(outerSig).append(", total cost:").append(cost).append("ms\n"); 134 | Map map = ctx.traceContent; 135 | map.forEach((k, v) -> { 136 | if (v[0] != 0 || !ignoreZero) { 137 | sb.append(">>").append(k).append(" hit:").append(v[1]).append("times, total cost:").append(v[0]).append("ms\n"); 138 | } 139 | }); 140 | w.Global.info(sb); 141 | } 142 | traceCtx.remove(); 143 | } 144 | } 145 | 146 | public static void recursiveRecord(String uuid) { 147 | traceCtx.get().computeIfAbsent(uuid, k -> new TraceCtx()).stackDeep++; 148 | } 149 | 150 | public boolean equals(Object other) { 151 | if (other instanceof OuterWatchTransformer) { 152 | return this.uuid.equals(((OuterWatchTransformer) other).getUuid()); 153 | } 154 | return false; 155 | } 156 | 157 | @Override 158 | public String desc() { 159 | return "Trace_" + getClassName() + "#" + method; 160 | } 161 | 162 | @Override 163 | public void clear() { 164 | traceCtx.get().remove(this.uuid.toString()); 165 | } 166 | 167 | public static class TraceCtx { 168 | int stackDeep = 1; 169 | Map traceContent = new LinkedHashMap<>(); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/main/java/w/core/model/WatchTransformer.java: -------------------------------------------------------------------------------- 1 | package w.core.model; 2 | 3 | import javassist.*; 4 | import lombok.Data; 5 | import org.objectweb.asm.*; 6 | import w.Global; 7 | import w.core.asm.SbNode; 8 | import w.core.asm.WAdviceAdapter; 9 | import w.util.RequestUtils; 10 | import w.web.message.WatchMessage; 11 | 12 | 13 | import java.io.FileOutputStream; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | import java.util.Objects; 17 | import java.util.concurrent.atomic.AtomicBoolean; 18 | 19 | import static org.objectweb.asm.Opcodes.*; 20 | 21 | 22 | /** 23 | * @author Frank 24 | * @date 2023/12/21 13:46 25 | */ 26 | @Data 27 | public class WatchTransformer extends BaseClassTransformer { 28 | 29 | transient WatchMessage message; 30 | 31 | String method; 32 | 33 | int printFormat; 34 | 35 | int minCost; 36 | 37 | public WatchTransformer(WatchMessage watchMessage) { 38 | this.className = watchMessage.getSignature().split("#")[0]; 39 | this.method = watchMessage.getSignature().split("#")[1]; 40 | this.message = watchMessage; 41 | this.traceId = watchMessage.getId(); 42 | this.printFormat = watchMessage.getPrintFormat(); 43 | this.minCost = watchMessage.getMinCost(); 44 | } 45 | 46 | @Override 47 | public byte[] transform(byte[] origin) throws Exception { 48 | ClassReader classReader = new ClassReader(origin); 49 | ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES) { 50 | @Override 51 | protected ClassLoader getClassLoader() { 52 | return Global.getClassLoader(); 53 | } 54 | }; 55 | 56 | AtomicBoolean effect = new AtomicBoolean(); 57 | classReader.accept(new ClassVisitor(ASM9, classWriter) { 58 | @Override 59 | public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { 60 | MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); 61 | if (!name.equals(method)) return mv; 62 | return new WAdviceAdapter(ASM9, mv, access, name, descriptor) { 63 | private int startTimeVarIndex; 64 | private int paramsVarIndex; 65 | 66 | private int returnValueVarIndex = -1; 67 | 68 | private int exceptionStringIndex = -1; 69 | 70 | 71 | private int methodSignatureVarIndex; 72 | private final Label startTry = new Label(); 73 | private final Label endTry = new Label(); 74 | private final Label startCatch = new Label(); 75 | private final Label endCatch = new Label(); 76 | 77 | @Override 78 | protected void onMethodEnter() { 79 | /*---------------------startTime:long start = System.currentTimeMillis();-----------------*/ 80 | startTimeVarIndex = asmStoreStartTime(mv); 81 | /*---------------------param: String params = Arrays.toString(paramsArray);-----------------*/ 82 | paramsVarIndex = asmStoreParamsString(mv, printFormat); 83 | methodSignatureVarIndex = newLocal(Type.getType(String.class)); 84 | String methodSignature = className.substring(1 + className.lastIndexOf('.')) +"#" + method; 85 | mv.visitLdcInsn(methodSignature); 86 | mv.visitVarInsn(ASTORE, methodSignatureVarIndex); 87 | mv.visitLdcInsn(traceId); 88 | mv.visitMethodInsn(INVOKESTATIC, "w/util/RequestUtils", "fillCurThread", "(Ljava/lang/String;)V", false); 89 | // try { original } 90 | mv.visitLabel(startTry); 91 | } 92 | 93 | @Override 94 | protected void onMethodExit(int opcode) { 95 | /*---------------------returnValue: return object tostring, return the variable index------------*/ 96 | if (opcode != ATHROW) { 97 | returnValueVarIndex = asmStoreRetString(mv, descriptor, printFormat); 98 | postProcess(false); 99 | } 100 | } 101 | 102 | @Override 103 | public void visitMaxs(int maxStack, int maxLocals) { 104 | mv.visitLabel(endTry); 105 | mv.visitLabel(startCatch); 106 | int exceptionIndex = newLocal(Type.getType(Throwable.class)); 107 | mv.visitVarInsn(Opcodes.ASTORE, exceptionIndex); 108 | mv.visitVarInsn(Opcodes.ALOAD, exceptionIndex); 109 | exceptionStringIndex = newLocal(Type.getType(String.class)); 110 | mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "toString", "()Ljava/lang/String;", false); 111 | mv.visitVarInsn(Opcodes.ASTORE, exceptionStringIndex); 112 | 113 | postProcess(true); 114 | mv.visitVarInsn(Opcodes.ALOAD, exceptionIndex); 115 | mv.visitInsn(Opcodes.ATHROW); 116 | mv.visitLabel(endCatch); 117 | mv.visitTryCatchBlock(startTry, endTry, startCatch, "java/lang/Throwable"); 118 | effect.set(true); 119 | super.visitMaxs(maxStack, maxLocals); 120 | } 121 | 122 | private void postProcess(boolean whenThrow) { 123 | loadLocal(startTimeVarIndex, Type.LONG_TYPE); 124 | push(minCost); 125 | push(uuid.toString()); 126 | push(traceId); 127 | loadLocal(methodSignatureVarIndex, Type.getType(String.class)); 128 | loadLocal(paramsVarIndex, Type.getType(String.class)); 129 | 130 | if (whenThrow) { 131 | mv.visitInsn(Opcodes.ACONST_NULL); 132 | loadLocal(exceptionStringIndex, Type.getType(String.class)); 133 | } else { 134 | loadLocal(returnValueVarIndex, Type.getType(String.class)); 135 | mv.visitInsn(Opcodes.ACONST_NULL); 136 | } 137 | mv.visitMethodInsn(INVOKESTATIC, "w/core/asm/Tool", "watchPostProcess", "(JILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", false); 138 | } 139 | 140 | }; 141 | } 142 | }, ClassReader.EXPAND_FRAMES ); 143 | byte[] result = classWriter.toByteArray(); 144 | if (!effect.get()) { 145 | throw new IllegalArgumentException("Method not declared here."); 146 | } 147 | status = 1; 148 | return result; 149 | } 150 | 151 | public boolean equals(Object other) { 152 | if (other instanceof WatchTransformer) { 153 | return this.uuid.equals(((WatchTransformer) other).getUuid()); 154 | } 155 | return false; 156 | } 157 | 158 | @Override 159 | public String desc() { 160 | return "Watch_" + getClassName() + "#" + method; 161 | } 162 | 163 | 164 | 165 | } 166 | -------------------------------------------------------------------------------- /src/main/java/w/util/NativeUtils.java: -------------------------------------------------------------------------------- 1 | package w.util; 2 | 3 | import java.io.*; 4 | import java.nio.file.FileSystemNotFoundException; 5 | import java.nio.file.FileSystems; 6 | import java.nio.file.Files; 7 | import java.nio.file.ProviderNotFoundException; 8 | import java.nio.file.StandardCopyOption; 9 | 10 | public class NativeUtils { 11 | 12 | /** 13 | * The minimum length a prefix for a file has to have according to {@link File#createTempFile(String, String)}}. 14 | */ 15 | private static final int MIN_PREFIX_LENGTH = 3; 16 | public static final String NATIVE_FOLDER_PATH_PREFIX = "nativeutils"; 17 | 18 | /** 19 | * Temporary directory which will contain the DLLs. 20 | */ 21 | private static File temporaryDir; 22 | 23 | /** 24 | * Private constructor - this class will never be instanced 25 | */ 26 | private NativeUtils() { 27 | } 28 | 29 | /** 30 | * Loads library from current JAR archive 31 | * 32 | * The file from JAR is copied into system temporary directory and then loaded. The temporary file is deleted after 33 | * exiting. 34 | * Method uses String as filename because the pathname is "abstract", not system-dependent. 35 | * 36 | * @param path The path of file inside JAR as absolute path (beginning with '/'), e.g. /package/File.ext 37 | * @throws IOException If temporary file creation or read/write operation fails 38 | * @throws IllegalArgumentException If source file (param path) does not exist 39 | * @throws IllegalArgumentException If the path is not absolute or if the filename is shorter than three characters 40 | * (restriction of {@link File#createTempFile(java.lang.String, java.lang.String)}). 41 | * @throws FileNotFoundException If the file could not be found inside the JAR. 42 | */ 43 | public static void loadLibraryFromJar(String path) throws IOException { 44 | 45 | if (null == path || !path.startsWith("/")) { 46 | throw new IllegalArgumentException("The path has to be absolute (start with '/')."); 47 | } 48 | 49 | // Obtain filename from path 50 | String[] parts = path.split("/"); 51 | String filename = (parts.length > 1) ? parts[parts.length - 1] : null; 52 | 53 | // Check if the filename is okay 54 | if (filename == null || filename.length() < MIN_PREFIX_LENGTH) { 55 | throw new IllegalArgumentException("The filename has to be at least 3 characters long."); 56 | } 57 | 58 | // Prepare temporary file 59 | if (temporaryDir == null) { 60 | temporaryDir = createTempDirectory(NATIVE_FOLDER_PATH_PREFIX); 61 | temporaryDir.deleteOnExit(); 62 | } 63 | 64 | File temp = new File(temporaryDir, filename); 65 | 66 | try (InputStream is = NativeUtils.class.getResourceAsStream(path)) { 67 | Files.copy(is, temp.toPath(), StandardCopyOption.REPLACE_EXISTING); 68 | } catch (IOException e) { 69 | temp.delete(); 70 | throw e; 71 | } catch (NullPointerException e) { 72 | temp.delete(); 73 | throw new FileNotFoundException("File " + path + " was not found inside JAR."); 74 | } 75 | 76 | try { 77 | System.load(temp.getAbsolutePath()); 78 | } finally { 79 | if (isPosixCompliant()) { 80 | // Assume POSIX compliant file system, can be deleted after loading 81 | temp.delete(); 82 | } else { 83 | // Assume non-POSIX, and don't delete until last file descriptor closed 84 | temp.deleteOnExit(); 85 | } 86 | } 87 | } 88 | 89 | private static boolean isPosixCompliant() { 90 | try { 91 | return FileSystems.getDefault() 92 | .supportedFileAttributeViews() 93 | .contains("posix"); 94 | } catch (FileSystemNotFoundException 95 | | ProviderNotFoundException 96 | | SecurityException e) { 97 | return false; 98 | } 99 | } 100 | 101 | private static File createTempDirectory(String prefix) throws IOException { 102 | String tempDir = System.getProperty("java.io.tmpdir"); 103 | File generatedDir = new File(tempDir, prefix + System.nanoTime()); 104 | 105 | if (!generatedDir.mkdir()) 106 | throw new IOException("Failed to create temp directory " + generatedDir.getName()); 107 | 108 | return generatedDir; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/w/util/RequestUtils.java: -------------------------------------------------------------------------------- 1 | package w.util; 2 | 3 | import fi.iki.elonen.NanoWSD; 4 | import w.core.compiler.WCompiler; 5 | 6 | import java.lang.instrument.ClassFileTransformer; 7 | import java.util.*; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | 10 | /** 11 | * @author Frank 12 | * @date 2023/12/21 11:43 13 | */ 14 | public class RequestUtils { 15 | private final static ThreadLocal socketCtx = new ThreadLocal<>(); 16 | private final static ThreadLocal traceIdCtx = new ThreadLocal<>(); 17 | private final static ThreadLocal> classNameToByteCode = ThreadLocal.withInitial(HashMap::new); 18 | public final static Map traceId2Ws = new ConcurrentHashMap<>(); 19 | 20 | public static void initRequestCtx(NanoWSD.WebSocket ws, String traceId) { 21 | socketCtx.set(ws); 22 | traceIdCtx.set(traceId); 23 | if (traceId != null && ws != null) { 24 | traceId2Ws.put(traceId, ws); 25 | } 26 | } 27 | 28 | public static void clearRequestCtx() { 29 | socketCtx.remove(); 30 | traceIdCtx.remove(); 31 | classNameToByteCode.remove(); 32 | } 33 | 34 | public String getTraceId() { 35 | return traceIdCtx.get(); 36 | } 37 | 38 | public static NanoWSD.WebSocket getCurWs() { 39 | return socketCtx.get(); 40 | } 41 | 42 | public static void fillCurThread(String traceId) { 43 | initRequestCtx(traceId2Ws.get(traceId), traceId); 44 | } 45 | 46 | public static String getCurTraceId() { 47 | return traceIdCtx.get(); 48 | } 49 | 50 | public static NanoWSD.WebSocket getWsByTraceId(String traceId) { 51 | return traceId2Ws.get(traceId); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/w/util/SpringUtils.java: -------------------------------------------------------------------------------- 1 | package w.util; 2 | 3 | import javassist.LoaderClassPath; 4 | import lombok.Data; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import ognl.*; 8 | import w.Global; 9 | 10 | import java.io.*; 11 | import java.lang.management.ManagementFactory; 12 | import java.lang.management.RuntimeMXBean; 13 | import java.lang.reflect.InvocationTargetException; 14 | import java.net.URL; 15 | import java.nio.file.Files; 16 | import java.nio.file.Path; 17 | import java.nio.file.Paths; 18 | import java.util.*; 19 | import java.util.jar.JarEntry; 20 | import java.util.jar.JarFile; 21 | 22 | /** 23 | * @author Frank 24 | * @date 2023/12/21 11:26 25 | */ 26 | public class SpringUtils { 27 | @Getter 28 | static ClassLoader springBootClassLoader; 29 | 30 | @Getter 31 | static Object springBootApplicationContext; 32 | 33 | static final String APP_CTX_CLASS_NAME = "org.springframework.context.ApplicationContext"; 34 | 35 | public static boolean isSpring() { 36 | return springBootApplicationContext != null; 37 | } 38 | 39 | public static String getAppCtxClassName() { 40 | return APP_CTX_CLASS_NAME; 41 | } 42 | 43 | 44 | public static void initFromLoadedClasses() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { 45 | Class[] loadedClasses = Global.instrumentation.getAllLoadedClasses(); 46 | Set classLoaders = new HashSet<>(); 47 | for (Class c : loadedClasses) { 48 | // if it is a spring boot fat jar, the class loader will be LaunchedURLClassLoader, for spring boot >1 and <3 49 | if (c.getClassLoader() == null) continue; 50 | if (classLoaders.add(c.getClassLoader())) Global.classPool.appendClassPath(new LoaderClassPath(c.getClassLoader())); 51 | if (c.getName().equals(SpringUtils.getAppCtxClassName())) { 52 | Object[] instances = Global.getInstances(c); 53 | int max = -1; 54 | Object leader = null; 55 | for (Object instance : instances) { 56 | int count = (int) instance.getClass().getMethod("getBeanDefinitionCount").invoke(instance); 57 | if (count > max) { 58 | max = count; 59 | leader = instance; 60 | } 61 | } 62 | ClassLoader cl = c.getClassLoader(); 63 | System.out.println("find springboot application context is loaded by " + cl); 64 | SpringUtils.springBootApplicationContext = leader; 65 | SpringUtils.springBootClassLoader = c.getClassLoader(); 66 | break; 67 | } 68 | } 69 | 70 | 71 | RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean(); 72 | List inputArguments = runtimeMXBean.getInputArguments(); 73 | String xverifyValue = null; 74 | for (String a : inputArguments) { 75 | if (a.startsWith("-Xverify:")) { 76 | xverifyValue = a.substring("-Xverify:".length()); 77 | break; 78 | } 79 | } 80 | if (Objects.equals("none", xverifyValue)) { 81 | Global.nonVerifying = true; 82 | } 83 | } 84 | 85 | public static String generateSpringCtxCode() { 86 | if (!isSpring()) { 87 | return ""; 88 | } 89 | return String.format("%s ctx = (%s) (%s).getSpringBootApplicationContext();\n", 90 | APP_CTX_CLASS_NAME, APP_CTX_CLASS_NAME, SpringUtils.class.getName()); 91 | } 92 | 93 | } -------------------------------------------------------------------------------- /src/main/java/w/util/WClassLoader.java: -------------------------------------------------------------------------------- 1 | package w.util; 2 | 3 | import java.net.URL; 4 | import java.net.URLClassLoader; 5 | 6 | /** 7 | * @author Frank 8 | * @date 2024/4/4 17:09 9 | */ 10 | public class WClassLoader extends URLClassLoader { 11 | 12 | public WClassLoader(URL[] urls, ClassLoader parent) { 13 | super(urls, parent); 14 | } 15 | 16 | @Override 17 | protected Class findClass(String name) throws ClassNotFoundException { 18 | return super.findClass(name); 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/java/w/web/Httpd.java: -------------------------------------------------------------------------------- 1 | package w.web; 2 | 3 | import fi.iki.elonen.NanoHTTPD; 4 | import lombok.extern.slf4j.Slf4j; 5 | import w.Global; 6 | import w.core.compiler.WCompiler; 7 | 8 | import java.io.*; 9 | import java.nio.charset.StandardCharsets; 10 | import java.nio.file.Files; 11 | import java.nio.file.Paths; 12 | import java.util.Map; 13 | import java.util.Objects; 14 | import java.util.stream.Collectors; 15 | 16 | import static fi.iki.elonen.NanoHTTPD.Response.Status.*; 17 | 18 | /** 19 | * @author Frank 20 | * @date 2023/11/25 16:12 21 | */ 22 | public class Httpd extends NanoHTTPD { 23 | 24 | public Httpd(int port) { 25 | super(port); 26 | } 27 | 28 | /** 29 | * 30 | * @param uri 31 | * Percent-decoded URI without parameters, for example 32 | * "/index.cgi" 33 | * @param method 34 | * "GET", "POST" etc. 35 | * @param header 36 | * Header entries, percent decoded 37 | * @param parameters 38 | * Parsed, percent decoded parameters from URI and, in case of 39 | * POST, form data. 40 | * @param files 41 | * POST json body, the key is postData, value is body string 42 | * @return 43 | */ 44 | @Override 45 | public Response serve(String uri, Method method, 46 | Map header, Map parameters, 47 | Map files) { 48 | if (method == Method.GET) { 49 | // deprecated, plz use ws reset 50 | if (uri.equals("/reset")) { 51 | Global.reset(); 52 | return newFixedLengthResponse("ok"); 53 | } 54 | return serveFile(uri); 55 | } 56 | if (method == Method.POST) { 57 | switch (uri) { 58 | case "/wsPort": 59 | return newFixedLengthResponse(Global.wsPort + ""); 60 | } 61 | } 62 | 63 | return newFixedLengthResponse(BAD_REQUEST, "", "NOT SUPPORT POST"); 64 | } 65 | 66 | private Response serveFile(String fileName) { 67 | if (fileName == null) { 68 | return newFixedLengthResponse(NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "NOT FOUND"); 69 | } 70 | 71 | // default lead to index.html 72 | if (fileName.isEmpty() || fileName.equals("/")) { 73 | fileName = "/index.html"; 74 | } 75 | fileName = fileName.startsWith("/") ? fileName : ("/" + fileName); 76 | byte[] content = new byte[409600]; //400k of content 77 | int len = 0; 78 | 79 | String res = ""; 80 | try (InputStream in = this.getClass().getResourceAsStream("/nanohttpd" + fileName); 81 | BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { 82 | res = reader.lines().collect(Collectors.joining("\n")); 83 | } catch (Exception e) { 84 | return newFixedLengthResponse(NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "NOT FOUND"); 85 | } 86 | String mimeType = MIME_HTML; 87 | if (fileName.endsWith(".js")) { 88 | mimeType = "application/javascript"; 89 | } else if (fileName.endsWith(".css")) { 90 | mimeType = "text/css"; 91 | } 92 | return newFixedLengthResponse(OK, mimeType, res); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/w/web/Websocketd.java: -------------------------------------------------------------------------------- 1 | package w.web; 2 | 3 | import w.Global; 4 | import w.core.ExecBundle; 5 | import w.core.GroovyBundle; 6 | import w.core.Swapper; 7 | import w.util.RequestUtils; 8 | import w.web.message.*; 9 | import com.fasterxml.jackson.core.JsonProcessingException; 10 | import com.fasterxml.jackson.databind.ObjectMapper; 11 | import fi.iki.elonen.NanoWSD; 12 | 13 | import java.io.IOException; 14 | import java.io.PrintWriter; 15 | import java.io.StringWriter; 16 | import java.util.UUID; 17 | import java.util.logging.Logger; 18 | 19 | /** 20 | * @author Frank 21 | * @date 2023/11/25 16:46 22 | */ 23 | 24 | public class Websocketd extends NanoWSD { 25 | Logger log = Logger.getLogger(Websocketd.class.getName()); 26 | 27 | ObjectMapper objectMapper = new ObjectMapper(); 28 | 29 | Swapper swapper = Swapper.getInstance(); 30 | 31 | public Websocketd(int port) { 32 | super(port); 33 | } 34 | @Override 35 | protected WebSocket openWebSocket(IHTTPSession ihttpSession) { 36 | return new WebSocket(ihttpSession) { 37 | @Override 38 | protected void onOpen() { 39 | Global.addWs(this); 40 | } 41 | @Override 42 | protected void onClose(WebSocketFrame.CloseCode closeCode, String s, boolean b) { 43 | Global.removeWs(this); 44 | } 45 | 46 | @Override 47 | protected void onMessage(WebSocketFrame frame) { 48 | Global.addWs(this); 49 | frame.setUnmasked(); 50 | String msg = frame.getTextPayload(); 51 | dispatch(msg); 52 | } 53 | 54 | @Override 55 | protected void onPong(WebSocketFrame webSocketFrame) {} 56 | 57 | @Override 58 | protected void onException(IOException e) { 59 | if (!this.isOpen()) { 60 | System.out.println("ws closed"); 61 | Global.removeWs(this); 62 | } 63 | } 64 | 65 | private void dispatch(String msg) { 66 | try { 67 | Message message = objectMapper.readValue(msg, Message.class); 68 | if (message.getType() != MessageType.PING) { 69 | log.info(objectMapper.writeValueAsString(message)); 70 | RequestUtils.initRequestCtx(this, message.getId()); 71 | } 72 | switch (message.getType()) { 73 | case PING: 74 | PongMessage m = new PongMessage(); 75 | m.setId(message.getId()); 76 | String json = objectMapper.writeValueAsString(m); 77 | this.send(json); 78 | break; 79 | case EXEC: 80 | ExecMessage execMessage = (ExecMessage) message; 81 | ExecBundle.changeBodyAndInvoke(execMessage.getBody()); 82 | break; 83 | case EVAL: 84 | EvalMessage evalMessage = (EvalMessage) message; 85 | try { 86 | Object res = GroovyBundle.eval(evalMessage.getBody()); 87 | Global.info((evalMessage.getBody().startsWith("!") ? 88 | "$ " + evalMessage.getBody().substring(1) : "groovy > " + evalMessage.getBody()) + "\n> " + res); 89 | } catch (Exception e) { 90 | Global.error(e.toString(), e); 91 | } 92 | break; 93 | case DELETE: 94 | DeleteMessage deleteMessage = (DeleteMessage) message; 95 | if (deleteMessage.getUuid() != null) { 96 | try { 97 | Global.deleteTransformer(UUID.fromString(deleteMessage.getUuid())); 98 | } catch (Exception e) { 99 | Global.error("delete error:", e); 100 | } 101 | } 102 | break; 103 | case RESET: 104 | Global.reset(); 105 | Global.info("reset finished"); 106 | break; 107 | default: 108 | swapper.swap(message); 109 | } 110 | 111 | } catch (JsonProcessingException e) { 112 | e.printStackTrace(); 113 | Global.error("not a valid message"); 114 | } catch (Throwable e) { 115 | Global.error("error:", e); 116 | } finally { 117 | RequestUtils.clearRequestCtx(); 118 | } 119 | } 120 | }; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/w/web/message/ChangeBodyMessage.java: -------------------------------------------------------------------------------- 1 | package w.web.message; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * request message to change the java method body 9 | * 10 | * @author Frank 11 | * @date 2023/11/25 22:08 12 | */ 13 | @Data 14 | public class ChangeBodyMessage extends Message implements RequestMessage { 15 | String className; 16 | String method; 17 | List paramTypes; 18 | String body; 19 | int mode = 1; 20 | { 21 | type = MessageType.CHANGE_BODY; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/w/web/message/ChangeResultMessage.java: -------------------------------------------------------------------------------- 1 | package w.web.message; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * request message to change the java method body 9 | * 10 | * @author Frank 11 | * @date 2023/11/25 22:08 12 | */ 13 | @Data 14 | public class ChangeResultMessage extends Message implements RequestMessage { 15 | String className; 16 | String method; 17 | List paramTypes; 18 | String innerClassName; 19 | String innerMethod; 20 | String body; 21 | int mode; 22 | { 23 | type = MessageType.CHANGE_RESULT; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/w/web/message/DecompileMessage.java: -------------------------------------------------------------------------------- 1 | package w.web.message; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * request message to execute some code in a new thread 7 | * @author Frank 8 | * @date 2023/11/25 22:08 9 | */ 10 | @Data 11 | public class DecompileMessage extends Message implements RequestMessage { 12 | { 13 | type = MessageType.DECOMPILE; 14 | } 15 | 16 | String className; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/w/web/message/DeleteMessage.java: -------------------------------------------------------------------------------- 1 | package w.web.message; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * request message to execute some code in a new thread 7 | * @author Frank 8 | * @date 2023/11/25 22:08 9 | */ 10 | @Data 11 | public class DeleteMessage extends Message implements RequestMessage { 12 | { 13 | type = MessageType.DELETE; 14 | } 15 | 16 | String uuid; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/w/web/message/EvalMessage.java: -------------------------------------------------------------------------------- 1 | package w.web.message; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * request message to execute some code in a new thread 7 | * @author Frank 8 | * @date 2023/11/25 22:08 9 | */ 10 | @Data 11 | public class EvalMessage extends Message implements RequestMessage { 12 | { 13 | type = MessageType.EVAL; 14 | } 15 | 16 | String body; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/w/web/message/ExecMessage.java: -------------------------------------------------------------------------------- 1 | package w.web.message; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * request message to execute some code in a new thread 7 | * @author Frank 8 | * @date 2023/11/25 22:08 9 | */ 10 | @Data 11 | public class ExecMessage extends Message implements RequestMessage { 12 | { 13 | type = MessageType.EXEC; 14 | } 15 | 16 | String body; 17 | 18 | int mode; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/w/web/message/LogMessage.java: -------------------------------------------------------------------------------- 1 | package w.web.message; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * Response message with the server log that happen in this request 7 | * @author Frank 8 | * @date 2023/11/25 22:51 9 | */ 10 | @Data 11 | public class LogMessage extends Message { 12 | { 13 | type = MessageType.LOG; 14 | } 15 | int level; 16 | String content; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/w/web/message/Message.java: -------------------------------------------------------------------------------- 1 | package w.web.message; 2 | 3 | import com.fasterxml.jackson.annotation.JsonSubTypes; 4 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 5 | import lombok.Data; 6 | 7 | import java.util.UUID; 8 | 9 | /** 10 | * 11 | * Message abstract class, to json ser/des 12 | * @author Frank 13 | * @date 2023/11/25 21:52 14 | */ 15 | 16 | @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") 17 | @JsonSubTypes({ 18 | @JsonSubTypes.Type(value = ReplaceClassMessage.class, name = "REPLACE_CLASS"), 19 | @JsonSubTypes.Type(value = ChangeBodyMessage.class, name = "CHANGE_BODY"), 20 | @JsonSubTypes.Type(value = ChangeResultMessage.class, name = "CHANGE_RESULT"), 21 | @JsonSubTypes.Type(value = PingMessage.class, name = "PING"), 22 | @JsonSubTypes.Type(value = PongMessage.class, name = "PONG"), 23 | @JsonSubTypes.Type(value = WatchMessage.class, name = "WATCH"), 24 | @JsonSubTypes.Type(value = OuterWatchMessage.class, name = "OUTER_WATCH"), 25 | @JsonSubTypes.Type(value = ExecMessage.class, name = "EXEC"), 26 | @JsonSubTypes.Type(value = TraceMessage.class, name = "TRACE"), 27 | @JsonSubTypes.Type(value = DeleteMessage.class, name = "DELETE"), 28 | @JsonSubTypes.Type(value = ResetMessage.class, name = "RESET"), 29 | @JsonSubTypes.Type(value = DecompileMessage.class, name = "DECOMPILE"), 30 | @JsonSubTypes.Type(value = EvalMessage.class, name = "EVAL"), 31 | }) 32 | @Data 33 | public abstract class Message { 34 | protected MessageType type; 35 | protected String id = UUID.randomUUID().toString(); 36 | 37 | protected long timestamp = System.currentTimeMillis(); 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/w/web/message/MessageType.java: -------------------------------------------------------------------------------- 1 | package w.web.message; 2 | 3 | /** 4 | * Message Type Enum 5 | * 6 | * @author Frank 7 | * @date 2023/11/25 21:58 8 | */ 9 | public enum MessageType { 10 | /** 11 | * 修改方法的body的消息 12 | */ 13 | CHANGE_BODY, 14 | CHANGE_RESULT, 15 | 16 | WATCH, 17 | 18 | OUTER_WATCH, 19 | 20 | EXEC, 21 | 22 | REPLACE_CLASS, 23 | 24 | PING, 25 | 26 | PONG, 27 | 28 | LOG, 29 | 30 | DELETE, 31 | 32 | TRACE, 33 | 34 | DECOMPILE, 35 | 36 | RESET, 37 | 38 | EVAL, 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/w/web/message/OuterWatchMessage.java: -------------------------------------------------------------------------------- 1 | package w.web.message; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * Watch method message 7 | * @author Frank 8 | * @date 2023/11/26 19:49 9 | */ 10 | @Data 11 | public class OuterWatchMessage extends Message implements RequestMessage { 12 | { 13 | type = MessageType.OUTER_WATCH; 14 | } 15 | 16 | /** 17 | * The method signature with format: com.example.A#func 18 | */ 19 | String signature; 20 | 21 | String innerSignature; 22 | 23 | int printFormat = 1; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/w/web/message/PingMessage.java: -------------------------------------------------------------------------------- 1 | package w.web.message; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * Heartbeats message 7 | * @author Frank 8 | * @date 2023/11/25 22:08 9 | */ 10 | @Data 11 | public class PingMessage extends Message implements RequestMessage { 12 | { 13 | type = MessageType.PING; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/w/web/message/PongMessage.java: -------------------------------------------------------------------------------- 1 | package w.web.message; 2 | 3 | import lombok.Data; 4 | import w.Global; 5 | 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.stream.Collectors; 10 | 11 | /** 12 | * Heartbeats message 13 | * @author Frank 14 | * @date 2023/11/25 22:08 15 | */ 16 | @Data 17 | public class PongMessage extends Message implements ResponseMessage { 18 | Map>> content = new HashMap<>(); 19 | { 20 | type = MessageType.PONG; 21 | synchronized (Global.class) { 22 | Global.activeTransformers.forEach((cls, loader2Transs) -> { 23 | loader2Transs.forEach((loader, transs) -> { 24 | content.computeIfAbsent(cls, o -> new HashMap<>()).computeIfAbsent(loader, o -> transs.stream().map(trans -> trans.getTraceId() + "_" + trans.desc() +"_" + trans.getUuid()).collect(Collectors.toList())); 25 | }); 26 | }); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/w/web/message/ReplaceClassMessage.java: -------------------------------------------------------------------------------- 1 | package w.web.message; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * request message to replace class 7 | * @author Frank 8 | * @date 2023/11/25 22:08 9 | */ 10 | @Data 11 | public class ReplaceClassMessage extends Message implements RequestMessage { 12 | { 13 | type = MessageType.REPLACE_CLASS; 14 | } 15 | 16 | String className; 17 | 18 | String content; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/w/web/message/RequestMessage.java: -------------------------------------------------------------------------------- 1 | package w.web.message; 2 | 3 | /** 4 | * @author Frank 5 | * @date 2023/12/1 21:01 6 | */ 7 | public interface RequestMessage { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/w/web/message/ResetMessage.java: -------------------------------------------------------------------------------- 1 | package w.web.message; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * request message to execute some code in a new thread 7 | * @author Frank 8 | * @date 2023/11/25 22:08 9 | */ 10 | @Data 11 | public class ResetMessage extends Message implements RequestMessage { 12 | { 13 | type = MessageType.RESET; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/w/web/message/ResponseMessage.java: -------------------------------------------------------------------------------- 1 | package w.web.message; 2 | 3 | /** 4 | * @author Frank 5 | * @date 2023/12/1 21:01 6 | */ 7 | public interface ResponseMessage { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/w/web/message/TraceMessage.java: -------------------------------------------------------------------------------- 1 | package w.web.message; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class TraceMessage extends Message { 7 | { 8 | type = MessageType.TRACE; 9 | } 10 | 11 | /** 12 | * The method signature with format: com.example.A#func 13 | */ 14 | String signature; 15 | 16 | int minCost; 17 | 18 | boolean ignoreZero; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/w/web/message/WatchMessage.java: -------------------------------------------------------------------------------- 1 | package w.web.message; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * Watch method message 7 | * @author Frank 8 | * @date 2023/11/26 19:49 9 | */ 10 | @Data 11 | public class WatchMessage extends Message implements RequestMessage { 12 | { 13 | type = MessageType.WATCH; 14 | } 15 | 16 | /** 17 | * The method signature with format: com.example.A#func 18 | */ 19 | String signature; 20 | 21 | int printFormat = 1; 22 | 23 | int minCost; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/resources/InlineWrapper.java: -------------------------------------------------------------------------------- 1 | {{package_placeholder}} 2 | import java.util.*; 3 | 4 | public class InlineWrapper { 5 | 6 | static Object $$ = null; 7 | 8 | public static void replace({{args_placeholder}}) { 9 | {{body_placeholder}} 10 | } 11 | 12 | public static {{return_type}} $proceed() { 13 | throw new IllegalStateException("Placeholder cannot be invoked"); 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/resources/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunwu51/JVMByteSwapTool/f7284208d845de5b20465391665b8fc3cbc8781d/src/main/resources/META-INF/MANIFEST.MF -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/wshade.com.fasterxml.jackson.databind.Module: -------------------------------------------------------------------------------- 1 | wshade.com.fasterxml.jackson.datatype.jsr310.JavaTimeModule 2 | -------------------------------------------------------------------------------- /src/main/resources/nanohttpd/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/resources/w_Global.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "w_Global.h" 5 | 6 | static jvmtiIterationControl JNICALL tagInstance(jlong class_tag, jlong size, jlong* tag_ptr, void* user_data) { 7 | int* totalCount = (int*)user_data; 8 | if (*totalCount > 100) { 9 | return JVMTI_ITERATION_ABORT; 10 | } 11 | (*totalCount)++; 12 | *tag_ptr = 1; 13 | return JVMTI_ITERATION_CONTINUE; 14 | } 15 | 16 | JNIEXPORT jobjectArray JNICALL Java_w_Global_getInstances 17 | (JNIEnv* env, jclass c, jclass cls) { 18 | jvmtiEnv* jvmti; 19 | JavaVM* vm; 20 | (*env)->GetJavaVM(env, &vm); 21 | 22 | (*vm)->GetEnv(vm, (void**)&jvmti, JVMTI_VERSION_1_0); 23 | 24 | jvmtiCapabilities capabilities; 25 | memset(&capabilities, 0, sizeof(jvmtiCapabilities)); 26 | capabilities.can_tag_objects = 1; 27 | (*jvmti)->AddCapabilities(jvmti, &capabilities); 28 | 29 | int totalCount = 0; 30 | jlong tag = 1; 31 | jint count; 32 | jobject* instances; 33 | // at most 100 34 | (*jvmti)->IterateOverInstancesOfClass(jvmti, cls, JVMTI_HEAP_OBJECT_EITHER, &tagInstance, &totalCount); 35 | (*jvmti)->GetObjectsWithTags(jvmti, 1, &tag, &count, &instances, NULL); 36 | 37 | jobjectArray result = (*env)->NewObjectArray(env, totalCount, cls, NULL); 38 | for (int i = 0; i < count; i++) { 39 | (*env)->SetObjectArrayElement(env, result, i, instances[i]); 40 | (*jvmti)->SetTag(jvmti, instances[i], 0); 41 | } 42 | 43 | (*jvmti)->Deallocate(jvmti, (unsigned char*)instances); 44 | return result; 45 | } -------------------------------------------------------------------------------- /src/main/resources/w_Global.h: -------------------------------------------------------------------------------- 1 | /* DO NOT EDIT THIS FILE - it is machine generated */ 2 | #include 3 | /* Header for class w_Global */ 4 | 5 | #ifndef _Included_w_Global 6 | #define _Included_w_Global 7 | #ifdef __cplusplus 8 | extern "C" { 9 | #endif 10 | /* 11 | * Class: w_Global 12 | * Method: getInstances 13 | * Signature: (Ljava/lang/Class;)[Ljava/lang/Object; 14 | */ 15 | JNIEXPORT jobjectArray JNICALL Java_w_Global_getInstances 16 | (JNIEnv *, jclass, jclass); 17 | 18 | #ifdef __cplusplus 19 | } 20 | #endif 21 | #endif 22 | -------------------------------------------------------------------------------- /src/main/resources/w_aarch64.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunwu51/JVMByteSwapTool/f7284208d845de5b20465391665b8fc3cbc8781d/src/main/resources/w_aarch64.dylib -------------------------------------------------------------------------------- /src/main/resources/w_amd64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunwu51/JVMByteSwapTool/f7284208d845de5b20465391665b8fc3cbc8781d/src/main/resources/w_amd64.dll -------------------------------------------------------------------------------- /src/main/resources/w_amd64.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunwu51/JVMByteSwapTool/f7284208d845de5b20465391665b8fc3cbc8781d/src/main/resources/w_amd64.dylib -------------------------------------------------------------------------------- /src/main/resources/w_amd64.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunwu51/JVMByteSwapTool/f7284208d845de5b20465391665b8fc3cbc8781d/src/main/resources/w_amd64.so -------------------------------------------------------------------------------- /src/test/java/w/core/AbstractService.java: -------------------------------------------------------------------------------- 1 | package w.core; 2 | 3 | /** 4 | * @author Frank 5 | * @date 2024/4/30 19:26 6 | */ 7 | public abstract class AbstractService implements MyInterface { 8 | public String normalParentMethod() { 9 | return "a"; 10 | } 11 | 12 | abstract String abstractParentMethod(); 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/w/core/ChangeBodyTest.java: -------------------------------------------------------------------------------- 1 | package w.core; 2 | 3 | import net.bytebuddy.agent.ByteBuddyAgent; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import w.Global; 9 | import w.web.message.ChangeBodyMessage; 10 | 11 | import java.io.IOException; 12 | import java.lang.instrument.Instrumentation; 13 | import java.util.Arrays; 14 | 15 | public class ChangeBodyTest { 16 | 17 | 18 | ChangeTarget target = new ChangeTarget(); 19 | 20 | Swapper swapper = Swapper.getInstance();; 21 | 22 | @BeforeAll 23 | public static void setUp() throws Exception { 24 | Instrumentation instrumentation = ByteBuddyAgent.install(); 25 | Global.instrumentation = instrumentation; 26 | Global.fillLoadedClasses(); 27 | System.setProperty("maxHit", "3"); 28 | } 29 | 30 | @BeforeEach 31 | public void reset() { 32 | Global.reset(); 33 | } 34 | 35 | @Test 36 | public void javassistTest() { 37 | 38 | ChangeBodyMessage msg = new ChangeBodyMessage(); 39 | msg.setClassName("w.core.ChangeTarget"); 40 | msg.setMethod("getName"); 41 | msg.setMode(0); 42 | msg.setParamTypes(Arrays.asList()); 43 | msg.setBody("{ return \"newName\";}"); 44 | Assertions.assertTrue(swapper.swap(msg)); 45 | System.out.println(target.getName()); 46 | } 47 | 48 | @Test 49 | public void asmTest() { 50 | ChangeBodyMessage msg = new ChangeBodyMessage(); 51 | msg.setClassName("w.core.ChangeTarget"); 52 | msg.setMethod("getName"); 53 | msg.setMode(1); 54 | msg.setParamTypes(Arrays.asList()); 55 | msg.setBody("{ try {w.Global.readFile(\"3.xml\");} catch(Exception e) {} return \"123\";}"); 56 | Assertions.assertTrue(swapper.swap(msg)); 57 | System.out.println(target.getName()); 58 | } 59 | 60 | @Test 61 | public void asmTest2() throws IOException, InterruptedException { 62 | ChangeBodyMessage msg = new ChangeBodyMessage(); 63 | msg.setClassName("w.core.ChangeTarget"); 64 | msg.setMethod("add"); 65 | msg.setMode(1); 66 | msg.setParamTypes(Arrays.asList("int", "double")); 67 | msg.setBody("{return $1 + $2 + 100; }"); 68 | Assertions.assertTrue(swapper.swap(msg)); 69 | System.out.println(target.add(1,1)); 70 | Assertions.assertEquals(102.0, target.add(1, 1)); 71 | } 72 | 73 | @Test 74 | public void asmTest4() throws IOException, InterruptedException { 75 | ChangeBodyMessage msg = new ChangeBodyMessage(); 76 | msg.setClassName("w.core.ChangeTarget"); 77 | msg.setMethod("hashCode"); 78 | msg.setMode(1); 79 | msg.setParamTypes(Arrays.asList()); 80 | msg.setBody("{return 1; }"); 81 | Assertions.assertTrue(swapper.swap(msg)); 82 | System.out.println(target.hashCode()); 83 | Assertions.assertEquals(1, target.hashCode()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/test/java/w/core/ChangeResultTest.java: -------------------------------------------------------------------------------- 1 | package w.core; 2 | 3 | import net.bytebuddy.agent.ByteBuddyAgent; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import w.Global; 9 | import w.core.constant.Codes; 10 | import w.web.message.ChangeBodyMessage; 11 | import w.web.message.ChangeResultMessage; 12 | 13 | import java.io.IOException; 14 | import java.lang.instrument.Instrumentation; 15 | import java.util.Arrays; 16 | 17 | public class ChangeResultTest { 18 | 19 | 20 | ChangeTarget target = new ChangeTarget(); 21 | 22 | Swapper swapper = Swapper.getInstance();; 23 | 24 | @BeforeAll 25 | public static void setUp() throws Exception { 26 | Instrumentation instrumentation = ByteBuddyAgent.install(); 27 | Global.instrumentation = instrumentation; 28 | Global.fillLoadedClasses(); 29 | System.setProperty("maxHit", "3"); 30 | } 31 | 32 | @BeforeEach 33 | public void reset() { 34 | Global.reset(); 35 | } 36 | 37 | @Test 38 | public void javassistTest() throws IOException, InterruptedException { 39 | ChangeResultMessage msg = new ChangeResultMessage(); 40 | msg.setClassName("w.core.ChangeTarget"); 41 | msg.setMethod("addWrapper"); 42 | msg.setParamTypes(Arrays.asList("int", "int")); 43 | msg.setInnerMethod("add"); 44 | msg.setInnerClassName("*"); 45 | msg.setBody("{try { $_ = 0;} catch (Exception e) {$_ = 1;}}"); 46 | Assertions.assertTrue(swapper.swap(msg)); 47 | System.out.println(target.addWrapper(1,1)); 48 | } 49 | 50 | 51 | @Test 52 | public void asmTest() throws IOException, InterruptedException { 53 | ChangeResultMessage msg = new ChangeResultMessage(); 54 | msg.setClassName("w.core.ChangeTarget"); 55 | msg.setMethod("addWrapper"); 56 | msg.setMode(Codes.changeResultModeUseASM); 57 | msg.setParamTypes(Arrays.asList("int", "int")); 58 | msg.setInnerMethod("add"); 59 | msg.setInnerClassName("*"); 60 | msg.setBody("try { $_ = 100 /(int)$_; } catch (Exception e) {w.Global.error(\"error\", e);$_ = 33;}"); 61 | Assertions.assertTrue(swapper.swap(msg)); 62 | System.out.println(target.addWrapper(1,1)); 63 | 64 | } 65 | 66 | @Test 67 | public void asmTest2() throws IOException, InterruptedException { 68 | ChangeResultMessage msg = new ChangeResultMessage(); 69 | msg.setClassName("w.core.ChangeTarget"); 70 | msg.setMethod("hello"); 71 | msg.setMode(Codes.changeResultModeUseASM); 72 | msg.setParamTypes(Arrays.asList()); 73 | msg.setInnerMethod("getName"); 74 | msg.setInnerClassName("*"); 75 | msg.setBody("{$_= \"10086\";}"); 76 | Assertions.assertTrue(swapper.swap(msg)); 77 | System.out.println(target.hello()); 78 | 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/test/java/w/core/ChangeTarget.java: -------------------------------------------------------------------------------- 1 | package w.core; 2 | 3 | import lombok.Data; 4 | import w.Global; 5 | 6 | import java.io.IOException; 7 | 8 | @Data 9 | public class ChangeTarget implements Runnable { 10 | int age; 11 | String name; 12 | 13 | public String toString() { 14 | return "toString"; 15 | } 16 | 17 | public double add(int a, double b) throws IOException, InterruptedException { 18 | return a + b; 19 | } 20 | 21 | @Override 22 | public void run() { 23 | System.out.println("run"); 24 | } 25 | 26 | public double addWrapper(int a, int b) throws IOException, InterruptedException { 27 | // this a 28 | // var6 = double(b) var5 = a var4 = this 29 | // var14 = double(100) 30 | // 0this 1a 2b 3this 4a 5b 6x 31 | return add(a, b) + 10000.0; 32 | } 33 | 34 | public String hello() { 35 | return "user will save: name=" + getName() + ", age=" + getAge(); 36 | } 37 | } -------------------------------------------------------------------------------- /src/test/java/w/core/DecompileTest.java: -------------------------------------------------------------------------------- 1 | package w.core; 2 | 3 | import net.bytebuddy.agent.ByteBuddyAgent; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import w.Global; 9 | import w.core.model.DecompileTransformer; 10 | import w.web.message.DecompileMessage; 11 | 12 | import java.lang.instrument.Instrumentation; 13 | 14 | /** 15 | * @author Frank 16 | * @date 2024/6/30 22:57 17 | */ 18 | public class DecompileTest { 19 | 20 | ChangeTarget target = new ChangeTarget(); 21 | 22 | Swapper swapper = Swapper.getInstance();; 23 | 24 | @BeforeAll 25 | public static void setUp() throws Exception { 26 | Instrumentation instrumentation = ByteBuddyAgent.install(); 27 | Global.instrumentation = instrumentation; 28 | Global.fillLoadedClasses(); 29 | System.setProperty("maxHit", "3"); 30 | } 31 | 32 | @BeforeEach 33 | public void reset() { 34 | Global.reset(); 35 | } 36 | 37 | 38 | @Test 39 | public void test() { 40 | new WatchTarget(); 41 | DecompileMessage msg = new DecompileMessage(); 42 | msg.setClassName("w.core.ChangeTarget"); 43 | Assertions.assertTrue(swapper.swap(msg)); 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/w/core/ExecuteTest.java: -------------------------------------------------------------------------------- 1 | package w.core; 2 | 3 | import net.bytebuddy.agent.ByteBuddyAgent; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import w.Global; 9 | import w.web.message.ExecMessage; 10 | 11 | import java.lang.instrument.Instrumentation; 12 | import java.lang.reflect.InvocationTargetException; 13 | 14 | /** 15 | * @author Frank 16 | * @date 2024/6/30 20:43 17 | */ 18 | public class ExecuteTest { 19 | 20 | 21 | ChangeTarget target = new ChangeTarget(); 22 | 23 | Swapper swapper = Swapper.getInstance();; 24 | 25 | @BeforeAll 26 | public static void setUp() throws Exception { 27 | Instrumentation instrumentation = ByteBuddyAgent.install(); 28 | Global.instrumentation = instrumentation; 29 | Global.fillLoadedClasses(); 30 | System.setProperty("maxHit", "3"); 31 | } 32 | 33 | @BeforeEach 34 | public void reset() { 35 | Global.reset(); 36 | } 37 | 38 | 39 | @Test 40 | public void test() throws Exception { 41 | ExecMessage message = new ExecMessage(); 42 | message.setBody("package w; public class Exec{ public void exec() { w.Global.info(\"hello\");} }"); 43 | ExecBundle.changeBodyAndInvoke(message.getBody()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/w/core/MyInterface.java: -------------------------------------------------------------------------------- 1 | package w.core; 2 | 3 | /** 4 | * @author Frank 5 | * @date 2024/4/30 19:29 6 | */ 7 | public interface MyInterface { 8 | default String interfaceDefaultMethod() { 9 | return "default"; 10 | } 11 | 12 | String interfaceMethod(); 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/w/core/OuterWatchTest.java: -------------------------------------------------------------------------------- 1 | package w.core; 2 | 3 | import net.bytebuddy.agent.ByteBuddyAgent; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import w.Global; 9 | import w.web.message.OuterWatchMessage; 10 | import w.web.message.WatchMessage; 11 | 12 | import java.lang.instrument.Instrumentation; 13 | 14 | /** 15 | * @author Frank 16 | * @date 2024/6/24 23:06 17 | */ 18 | public class OuterWatchTest { 19 | 20 | WatchTarget target = new WatchTarget(); 21 | 22 | Swapper swapper = Swapper.getInstance();; 23 | 24 | @BeforeAll 25 | public static void setUp() throws Exception { 26 | Instrumentation instrumentation = ByteBuddyAgent.install(); 27 | Global.instrumentation = instrumentation; 28 | Global.fillLoadedClasses(); 29 | System.setProperty("maxHit", "10"); 30 | } 31 | 32 | @BeforeEach 33 | public void reset() { 34 | Global.reset(); 35 | } 36 | 37 | @Test 38 | public void test() { 39 | OuterWatchMessage msg = new OuterWatchMessage(); 40 | msg.setSignature("w.core.WatchTarget#subMethodCall"); 41 | msg.setInnerSignature("w.core.WatchTarget#getAge"); 42 | Assertions.assertTrue(swapper.swap(msg)); 43 | 44 | 45 | OuterWatchMessage msg2 = new OuterWatchMessage(); 46 | msg2.setSignature("w.core.WatchTarget#subMethodCall"); 47 | msg2.setInnerSignature("*#add"); 48 | msg2.setPrintFormat(2); 49 | Assertions.assertTrue(swapper.swap(msg2)); 50 | 51 | OuterWatchMessage msg3 = new OuterWatchMessage(); 52 | msg3.setSignature("w.core.WatchTarget#subMethodCall"); 53 | msg3.setInnerSignature("*#div"); 54 | msg3.setPrintFormat(2); 55 | Assertions.assertTrue(swapper.swap(msg3)); 56 | 57 | try { 58 | target.subMethodCall(); 59 | } catch (Exception e) { 60 | System.out.println("\033[32m" + e.toString() + "\033[0m"); 61 | } 62 | } 63 | 64 | @Test 65 | public void expTest() { 66 | OuterWatchMessage msg = new OuterWatchMessage(); 67 | msg.setSignature("w.core.WatchTarget#subMethodCallExp"); 68 | msg.setInnerSignature("*#readFile"); 69 | Assertions.assertTrue(swapper.swap(msg)); 70 | target.doubleMethodWithParams(0.1); 71 | 72 | } 73 | 74 | @Test 75 | public void doubleReturnAndArrayParam2() { 76 | WatchMessage msg = new WatchMessage(); 77 | msg.setSignature("w.core.WatchTarget#doubleMethodWithParams"); 78 | msg.setPrintFormat(2); 79 | Assertions.assertTrue(swapper.swap(msg)); 80 | target.doubleMethodWithParams(0.1); 81 | } 82 | @Test 83 | public void toStringTest() { 84 | WatchMessage msg = new WatchMessage(); 85 | msg.setSignature("w.core.WatchTarget#toString"); 86 | Assertions.assertTrue(swapper.swap(msg)); 87 | System.out.println(new WatchTarget().toString()); 88 | } 89 | 90 | @Test 91 | public void runTest() { 92 | WatchMessage msg = new WatchMessage(); 93 | msg.setSignature("w.core.WatchTarget#run"); 94 | Assertions.assertTrue(swapper.swap(msg)); 95 | target.run(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/test/java/w/core/R.java: -------------------------------------------------------------------------------- 1 | package w.core; 2 | 3 | 4 | import java.util.UUID; 5 | 6 | /** 7 | * @author Frank 8 | * @date 2024/4/30 19:30 9 | */ 10 | public class R extends AbstractService { 11 | @Override 12 | String abstractParentMethod() { 13 | return "R.abstractParentMethod"; 14 | } 15 | 16 | @Override 17 | public String interfaceMethod() { 18 | return "R.interfaceMethod"; 19 | } 20 | 21 | public int recursive(int n) { 22 | System.out.println("recursive with " + n); 23 | if (n <= 1) return 1; 24 | UUID.randomUUID(); 25 | return recursive(n - 1) + recursive(n - 2); 26 | } 27 | 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/w/core/R2.java: -------------------------------------------------------------------------------- 1 | package w.core; 2 | 3 | /** 4 | * @author Frank 5 | * @date 2024/4/30 19:30 6 | */ 7 | public class R2 extends AbstractService { 8 | @Override 9 | String abstractParentMethod() { 10 | return "R2.abstractParentMethod"; 11 | } 12 | 13 | @Override 14 | public String interfaceMethod() { 15 | return "R2.interfaceMethod"; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/w/core/SwapperTest.java: -------------------------------------------------------------------------------- 1 | package w.core; 2 | 3 | import net.bytebuddy.agent.ByteBuddyAgent; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import w.Global; 9 | import w.core.constant.Codes; 10 | import w.web.message.*; 11 | 12 | import java.io.IOException; 13 | import java.lang.instrument.Instrumentation; 14 | import java.util.Arrays; 15 | 16 | 17 | /** 18 | * @author Frank 19 | * @date 2024/4/21 10:57 20 | */ 21 | class SwapperTest { 22 | 23 | Swapper swapper = Swapper.getInstance();; 24 | 25 | TestClass t = new TestClass(); 26 | 27 | R r = new R(); 28 | R2 r2 = new R2(); 29 | 30 | WatchTarget target = new WatchTarget(); 31 | 32 | @BeforeAll 33 | public static void setUp() throws Exception { 34 | Instrumentation instrumentation = ByteBuddyAgent.install(); 35 | Global.instrumentation = instrumentation; 36 | Global.fillLoadedClasses(); 37 | System.setProperty("maxHit", "3"); 38 | 39 | } 40 | 41 | @BeforeEach 42 | public void reset() { 43 | Global.reset(); 44 | } 45 | 46 | @Test 47 | public void watchTest() { 48 | WatchMessage watchMessage = new WatchMessage(); 49 | 50 | watchMessage.setSignature("w.core.WatchTarget#voidMethodWithNoParams"); 51 | Assertions.assertTrue(swapper.swap(watchMessage)); 52 | target.voidMethodWithNoParams(); 53 | 54 | // 55 | // watchMessage.setSignature("w.core.MyInterface#interfaceMethod"); 56 | // Assertions.assertTrue(swapper.swap(watchMessage)); 57 | // 58 | // watchMessage = new WatchMessage(); 59 | // watchMessage.setSignature("w.core.R#interfaceMethod"); 60 | // Assertions.assertTrue(swapper.swap(watchMessage)); 61 | // 62 | // 63 | //// watchMessage = new WatchMessage(); 64 | //// watchMessage.setSignature("w.core.AbstractService#normalParentMethod"); 65 | //// Assertions.assertTrue(swapper.swap(watchMessage)); 66 | // 67 | //// watchMessage = new WatchMessage(); 68 | //// watchMessage.setSignature("w.core.MyInterface#interfaceDefaultMethod"); 69 | //// Assertions.assertTrue(swapper.swap(watchMessage)); 70 | // 71 | // r.abstractParentMethod(); 72 | // r.interfaceMethod(); 73 | // r.normalParentMethod(); 74 | // r.interfaceDefaultMethod(); 75 | // r2.normalParentMethod(); 76 | // r2.interfaceDefaultMethod(); 77 | // 78 | //// WatchMessage watchMessage = new WatchMessage(); 79 | //// watchMessage.setSignature("w.core.TestClass#hello"); 80 | //// Assertions.assertTrue(swapper.swap(watchMessage)); 81 | //// new TestClass().hello("frank", "david", "Smith"); 82 | //// new TestClass().hello("frank", "david", "Smith"); 83 | //// new TestClass().hello("frank", "david", "Smith"); 84 | //// new TestClass().hello("frank", "david", "Smith"); 85 | //// new TestClass().hello("frank", "david", "Smith"); 86 | // 87 | // watchMessage = new WatchMessage(); 88 | // watchMessage.setSignature("w.core.TestClass#generateRandom"); 89 | // Assertions.assertTrue(swapper.swap(watchMessage)); 90 | // new TestClass().generateRandom(); 91 | } 92 | 93 | @Test 94 | public void outerWatchTest() { 95 | OuterWatchMessage message = new OuterWatchMessage(); 96 | message.setSignature("w.core.TestClass#wrapperHello"); 97 | message.setInnerSignature("*#hello"); 98 | Assertions.assertTrue(swapper.swap(message)); 99 | t.wrapperHello("world"); 100 | t.wrapperHello("world"); 101 | t.wrapperHello("world"); 102 | t.wrapperHello("world"); 103 | } 104 | 105 | @Test 106 | public void traceTest() { 107 | TraceMessage message = new TraceMessage(); 108 | message.setSignature("w.core.TestClass#wrapperHello"); 109 | message.setIgnoreZero(false); 110 | Assertions.assertTrue(swapper.swap(message)); 111 | t.wrapperHello("world"); 112 | } 113 | 114 | @Test 115 | public void changeBodyTest() { 116 | // javassist test 117 | ChangeBodyMessage message = new ChangeBodyMessage(); 118 | message.setClassName("w.core.TestClass"); 119 | message.setMethod("wrapperHello"); 120 | message.setMode(Codes.changeBodyModeUseJavassist); 121 | message.setParamTypes(Arrays.asList("java.lang.String")); 122 | message.setBody("{return java.util.UUID.randomUUID().toString();}"); 123 | Assertions.assertTrue(swapper.swap(message)); 124 | Assertions.assertTrue(t.wrapperHello("world").length() > 30); 125 | System.out.println(t.wrapperHello("world")); 126 | 127 | } 128 | 129 | @Test 130 | public void changeBodyAsmTest() { 131 | // asm test 132 | ChangeBodyMessage message = new ChangeBodyMessage(); 133 | message.setClassName("w.core.TestClass"); 134 | message.setMethod("wrapperHello"); 135 | message.setMode(Codes.changeBodyModeUseASM); 136 | message.setParamTypes(Arrays.asList("java.lang.String")); 137 | message.setBody("{return \"arg=\" + $1 + \", uuid=\" + java.util.UUID.randomUUID().toString();}"); 138 | Assertions.assertTrue(swapper.swap(message)); 139 | Assertions.assertTrue(t.wrapperHello("world").length() > 30); 140 | System.out.println(t.wrapperHello("world")); 141 | } 142 | 143 | @Test 144 | public void changeResultJavassistTest() { 145 | ChangeResultMessage message = new ChangeResultMessage(); 146 | message.setClassName("w.core.TestClass"); 147 | message.setMethod("wrapperHello"); 148 | message.setParamTypes(Arrays.asList("java.lang.String")); 149 | message.setInnerClassName("*"); 150 | message.setInnerMethod("hello"); 151 | message.setBody("{$_ = java.util.UUID.randomUUID().toString();}"); 152 | Assertions.assertTrue(swapper.swap(message)); 153 | Assertions.assertTrue(t.wrapperHello("world").length() > 30); 154 | } 155 | 156 | @Test 157 | public void execTest() throws Exception { 158 | ExecMessage message = new ExecMessage(); 159 | message.setBody("package w; public class Exec{ public void exec() { w.Global.info(\"hello\");} }"); 160 | ExecBundle.changeBodyAndInvoke(message.getBody()); 161 | } 162 | 163 | @Test 164 | public void traceRecursiveTest() { 165 | System.setProperty("maxHit", "322"); 166 | 167 | TraceMessage message = new TraceMessage(); 168 | message.setSignature("w.core.R#recursive"); 169 | message.setIgnoreZero(false); 170 | Assertions.assertTrue(swapper.swap(message)); 171 | for (int i = 0; i < 10; i++) { 172 | r.recursive(3); 173 | } 174 | } 175 | 176 | 177 | public String asmTest() { 178 | if (1 >= 0) { 179 | Global.info(1); 180 | } 181 | return "1"; 182 | } 183 | } -------------------------------------------------------------------------------- /src/test/java/w/core/Target.java: -------------------------------------------------------------------------------- 1 | package w.core; 2 | 3 | import java.io.*; 4 | import java.nio.file.Files; 5 | import java.nio.file.Paths; 6 | import java.util.List; 7 | 8 | /** 9 | * @author Frank 10 | * @date 2024/6/24 22:39 11 | */ 12 | public class Target implements Runnable { 13 | String name; 14 | int age; 15 | 16 | public void empty() {} 17 | 18 | public void voidMethodWithNoParams() { 19 | System.out.println("voidMethodWithNoParams"); 20 | } 21 | 22 | public double doubleMethodWithParams(double... params) { 23 | System.out.println("doubleMethodWithParams"); 24 | return params[0]; 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | System.out.println("toString"); 30 | return "name: " + name + ", age: " + age; 31 | } 32 | 33 | @Override 34 | public void run() { 35 | System.out.println("run"); 36 | } 37 | 38 | public String getName() { 39 | return name; 40 | } 41 | 42 | 43 | public int getAge() { 44 | return age; 45 | } 46 | 47 | 48 | public static List readFile(String path) throws IOException { 49 | return Files.readAllLines(Paths.get(path)); 50 | } 51 | 52 | public void tryCatchTest(String input) { 53 | try { 54 | System.out.println(Integer.parseInt(input)); 55 | } catch (Throwable e) { 56 | System.out.println("error"); 57 | throw e; 58 | } finally { 59 | System.out.println("finally"); 60 | 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/w/core/TestClass.java: -------------------------------------------------------------------------------- 1 | package w.core; 2 | 3 | import java.util.UUID; 4 | 5 | /** 6 | * @author Frank 7 | * @date 2024/4/21 11:02 8 | */ 9 | public class TestClass { 10 | 11 | public String hello(String name) { 12 | try { 13 | Thread.sleep(1000L); 14 | } catch (InterruptedException e) { 15 | throw new RuntimeException(e); 16 | } 17 | System.out.println("hello " + name); 18 | return "hello " + name; 19 | } 20 | 21 | public String wrapperHello(String name) { 22 | try { 23 | return hello(name + "!"); 24 | }catch (Exception e) { 25 | return "null"; 26 | } 27 | } 28 | 29 | public String hello(String name, String arg2, String arg3) { 30 | try { 31 | Thread.sleep(1000L); 32 | } catch (InterruptedException e) { 33 | throw new RuntimeException(e); 34 | } 35 | return "hello " + name + " " + arg2 + " " + arg3; 36 | } 37 | 38 | public double generateRandom() { 39 | double ran = Math.random(); 40 | System.out.println("ran="+ ran); 41 | return ran; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/w/core/TraceTest.java: -------------------------------------------------------------------------------- 1 | package w.core; 2 | 3 | import net.bytebuddy.agent.ByteBuddyAgent; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import w.Global; 9 | import w.core.model.TraceTransformer; 10 | import w.web.message.TraceMessage; 11 | 12 | import java.lang.instrument.Instrumentation; 13 | 14 | /** 15 | * @author Frank 16 | * @date 2024/6/26 22:46 17 | */ 18 | public class TraceTest { 19 | 20 | WatchTarget target = new WatchTarget(); 21 | 22 | Swapper swapper = Swapper.getInstance();; 23 | 24 | @BeforeAll 25 | public static void setUp() throws Exception { 26 | Instrumentation instrumentation = ByteBuddyAgent.install(); 27 | Global.instrumentation = instrumentation; 28 | Global.fillLoadedClasses(); 29 | } 30 | 31 | @BeforeEach 32 | public void reset() { 33 | Global.reset(); 34 | } 35 | 36 | @Test 37 | public void normalTest1() { 38 | TraceMessage msg = new TraceMessage(); 39 | msg.setSignature("w.core.WatchTarget#callManyMethod"); 40 | swapper.swap(msg); 41 | target.name = "test"; 42 | target.callManyMethod(); 43 | target.callManyMethod(); 44 | Assertions.assertTrue(TraceTransformer.traceCtx.get().isEmpty()); 45 | } 46 | 47 | @Test 48 | public void normalTest2() { 49 | TraceMessage msg = new TraceMessage(); 50 | msg.setSignature("w.core.WatchTarget#callManyMethod"); 51 | msg.setIgnoreZero(true); 52 | msg.setMinCost(28); 53 | swapper.swap(msg); 54 | target.name = "test"; 55 | target.callManyMethod(); 56 | target.callManyMethod(); 57 | Assertions.assertTrue(TraceTransformer.traceCtx.get().isEmpty()); 58 | } 59 | 60 | @Test 61 | public void recursiveTest1() { 62 | TraceMessage msg = new TraceMessage(); 63 | msg.setSignature("w.core.WatchTarget#fib"); 64 | swapper.swap(msg); 65 | target.fib(5); 66 | } 67 | 68 | 69 | @Test 70 | public void recursiveTest2() { 71 | TraceMessage msg = new TraceMessage(); 72 | msg.setSignature("w.core.WatchTarget#ow1"); 73 | swapper.swap(msg); 74 | target.ow1(1,1); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/test/java/w/core/WatchTarget.java: -------------------------------------------------------------------------------- 1 | package w.core; 2 | 3 | import w.core.asm.Tool; 4 | import w.core.model.WatchTransformer; 5 | import w.util.RequestUtils; 6 | 7 | import java.io.*; 8 | import java.nio.file.Files; 9 | import java.nio.file.Paths; 10 | import java.util.Arrays; 11 | import java.util.Base64; 12 | import java.util.List; 13 | 14 | /** 15 | * @author Frank 16 | * @date 2024/6/24 22:39 17 | */ 18 | public class WatchTarget implements Runnable { 19 | String name; 20 | int age; 21 | 22 | public void empty() {} 23 | 24 | public void voidMethodWithNoParams() { 25 | System.out.println("voidMethodWithNoParams"); 26 | } 27 | 28 | public double doubleMethodWithParams(double... params) { 29 | System.out.println("doubleMethodWithParams"); 30 | return params[0]; 31 | } 32 | 33 | @Override 34 | public String toString() { 35 | System.out.println("toString"); 36 | return "name: " + name + ", age: " + age; 37 | } 38 | 39 | @Override 40 | public void run() { 41 | System.out.println("run"); 42 | } 43 | 44 | public String getName() { 45 | return name; 46 | } 47 | 48 | 49 | public int getAge() { 50 | return age; 51 | } 52 | 53 | 54 | public static List readFile(String path) throws IOException { 55 | return Files.readAllLines(Paths.get(path)); 56 | } 57 | 58 | public void tryCatchTest(String input) { 59 | try { 60 | System.out.println(Integer.parseInt(input)); 61 | } catch (Throwable e) { 62 | System.out.println("error"); 63 | throw e; 64 | } finally { 65 | System.out.println("finally"); 66 | 67 | } 68 | } 69 | 70 | private double add(long a, Integer b, double c) { 71 | return a + b + c; 72 | } 73 | private int div(double a, double b) { 74 | return (int) a / (int) b ; 75 | } 76 | 77 | public void subMethodCall() { 78 | System.out.println("age=" + getAge()); 79 | System.out.println(add(1, 2, 3)); 80 | System.out.println(div(2, 0)); 81 | } 82 | 83 | public void subMethodCallExp() throws IOException { 84 | readFile("123"); 85 | } 86 | 87 | public static void sleep(long millis) { 88 | try { 89 | Thread.sleep(millis); 90 | } catch (InterruptedException e) { 91 | throw new RuntimeException(e); 92 | } 93 | } 94 | 95 | public String callManyMethod() { 96 | for (int i = 0; i < 10; i++) { 97 | sleep(1); 98 | System.out.println("name=" + getName()); 99 | } 100 | for (int i = 0; i < 5; i++) { 101 | sleep(3); 102 | System.out.println(add(1,1, 3)); 103 | } 104 | return "Hello World"; 105 | } 106 | 107 | public int fib(int n) { 108 | if (n <= 2) return 1; 109 | return fib(n - 1) + fib(n - 2); 110 | } 111 | 112 | public int[] arrayReturn1() { 113 | return new int[]{1,2}; 114 | } 115 | 116 | 117 | public String[] arrayReturn2() { 118 | return new String[]{"1"}; 119 | } 120 | 121 | public int ow1(int a, int b) { 122 | return ow1(a + b); 123 | } 124 | 125 | public int ow1(int c) { 126 | return (int) add(c, 0, 1); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/test/java/w/core/WatchTest.java: -------------------------------------------------------------------------------- 1 | package w.core; 2 | 3 | import net.bytebuddy.agent.ByteBuddyAgent; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import w.Global; 9 | import w.core.model.WatchTransformer; 10 | import w.web.message.DecompileMessage; 11 | import w.web.message.WatchMessage; 12 | 13 | import java.lang.instrument.Instrumentation; 14 | import java.util.Arrays; 15 | 16 | /** 17 | * @author Frank 18 | * @date 2024/6/24 23:06 19 | */ 20 | public class WatchTest { 21 | 22 | WatchTarget target = new WatchTarget(); 23 | 24 | Swapper swapper = Swapper.getInstance();; 25 | 26 | @BeforeAll 27 | public static void setUp() throws Exception { 28 | Instrumentation instrumentation = ByteBuddyAgent.install(); 29 | Global.instrumentation = instrumentation; 30 | Global.fillLoadedClasses(); 31 | System.setProperty("maxHit", "3"); 32 | } 33 | 34 | @BeforeEach 35 | public void reset() { 36 | Global.reset(); 37 | } 38 | 39 | @Test 40 | public void test() { 41 | WatchMessage msg = new WatchMessage(); 42 | msg.setSignature("w.core.WatchTarget#voidMethodWithNoParams"); 43 | Assertions.assertTrue(swapper.swap(msg)); 44 | WatchMessage msg2 = new WatchMessage(); 45 | msg2.setPrintFormat(2); 46 | msg2.setSignature("w.core.WatchTarget#doubleMethodWithParams"); 47 | Assertions.assertTrue(swapper.swap(msg2)); 48 | WatchMessage msg3 = new WatchMessage(); 49 | msg3.setSignature("w.core.WatchTarget#empty"); 50 | Assertions.assertTrue(swapper.swap(msg3)); 51 | 52 | WatchMessage msg4 = new WatchMessage(); 53 | msg4.setSignature("w.core.WatchTarget#tryCatchTest"); 54 | Assertions.assertTrue(swapper.swap(msg4)); 55 | 56 | WatchMessage msg5 = new WatchMessage(); 57 | msg5.setSignature("w.core.WatchTarget#readFile"); 58 | Assertions.assertTrue(swapper.swap(msg5)); 59 | target.voidMethodWithNoParams(); 60 | target.doubleMethodWithParams(0.1324, 0.243543, 0.325432); 61 | target.empty(); 62 | 63 | try { 64 | target.readFile("1.tdsafds"); 65 | } catch (Exception e) { 66 | System.out.println(e.toString()); 67 | } 68 | 69 | try { 70 | target.tryCatchTest("hello"); 71 | } catch (Exception e) { 72 | System.out.println("success throw"); 73 | } 74 | try { 75 | target.doubleMethodWithParams(); 76 | } catch (Exception e) { 77 | System.out.println("success throw"); 78 | } 79 | } 80 | 81 | @Test 82 | public void toStringTest() { 83 | WatchMessage msg = new WatchMessage(); 84 | msg.setSignature("w.core.WatchTarget#arrayReturn1"); 85 | Assertions.assertTrue(swapper.swap(msg)); 86 | System.out.println(Arrays.toString(new WatchTarget().arrayReturn1())); 87 | 88 | new WatchTarget(); 89 | WatchMessage msg2 = new WatchMessage(); 90 | msg2.setSignature("w.core.WatchTarget#arrayReturn2"); 91 | Assertions.assertTrue(swapper.swap(msg2)); 92 | System.out.println(Arrays.toString(new WatchTarget().arrayReturn2())); 93 | 94 | } 95 | 96 | @Test 97 | public void returnArrTest() { 98 | WatchMessage msg = new WatchMessage(); 99 | msg.setSignature("w.core.WatchTarget#toString"); 100 | Assertions.assertTrue(swapper.swap(msg)); 101 | Assertions.assertTrue(swapper.swap(msg)); 102 | Assertions.assertTrue(swapper.swap(msg)); 103 | Assertions.assertTrue(swapper.swap(msg)); 104 | System.out.println(new WatchTarget().toString()); 105 | 106 | new WatchTarget(); 107 | DecompileMessage msg2 = new DecompileMessage(); 108 | msg2.setClassName("w.core.WatchTarget"); 109 | Assertions.assertTrue(swapper.swap(msg2)); 110 | 111 | } 112 | 113 | @Test 114 | public void runTest() { 115 | WatchMessage msg = new WatchMessage(); 116 | msg.setSignature("w.core.WatchTarget#run"); 117 | Assertions.assertTrue(swapper.swap(msg)); 118 | target.run(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /sw-ico.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunwu51/JVMByteSwapTool/f7284208d845de5b20465391665b8fc3cbc8781d/sw-ico.png --------------------------------------------------------------------------------