├── .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 | 
3 |
4 | 
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 | 
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 | 
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 | 
3 |
4 | 
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 | 
24 |
25 | 现在你就可以体验`swapper`提供的各种功能了,例如`watch`某个方法,触发这个方法的时候,入参返回值和耗时将会被打印出来。
26 |
27 | 
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