diff --git a/README.md b/README_CN.md
similarity index 93%
rename from README.md
rename to README_CN.md
index 4147900..f6fca17 100644
--- a/README.md
+++ b/README_CN.md
@@ -18,15 +18,7 @@
# start
从github release下载`swapper-xx.jar`,运行指令,根据提示输入即可,如下:
```bash
-# java >=9
-$ java -jar swapper.jar
-
-# java == 8 Linux/MacOs:
-$ java -cp ${JAVA_HOME}/lib/tools.jar:swapper.jar w.Attach
-# java == 8 Windows
-$ java -cp "%JAVA_HOME%\lib\tools.jar";swapper.jar w.Attach
-
-
+$ java -jar swapper-0.0.1-SNAPSHOT.jar
[0] 36200 swapper.jar
[1] 55908 com.example.springweb.SpringWebApplication
[2] Custom PID
@@ -35,14 +27,10 @@ $ java -cp "%JAVA_HOME%\lib\tools.jar";swapper.jar w.Attach
============The PID is 55908
============Attach finish
```
-此时已经attach完成,到目标jvm的日志中可以看到如下log。
-
-![image](https://i.imgur.com/y8v0ptc.png)
-
# usage
-根据上面log中去请求页面,`http://localhost:8000` 默认是8000端口,如果出现冲突会替换,以上面log中为准。
+根据上面log中监听的http端口去请求页面,`http://localhost:8000` 默认是8000端口,如果出现冲突会替换,以上面log中为准。
-建议测试环境,jvm启动参数添加`-Xverify:none`,否则部分信息不会打印。
+建议测试环境,jvm启动参数添加`-Xverify:none`,否则部分异常信息日志不会打印。
注意!! 工具中所有的类名,只能是类,不能是接口。
## 1 watch
@@ -182,7 +170,8 @@ public class TestService {
可以通过effected class按钮查看当前被修改的类,也可以指定uuid剔除某些transformer,或者reset删除全部。
![image](https://github.com/sunwu51/JVMByteSwapTool/assets/15844103/3144aab1-c6a6-4df2-9737-6f0e503b36a2)
-
+# tui
+目前引入了tui,方便一些服务器不能ip直接访问,而无法打开页面的场景,tui-client在jbs-client目录,用法与网页类似,通过`tab`切换选项,`enter`进行选中。
diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml
index 1b39315..8a29dc3 100644
--- a/dependency-reduced-pom.xml
+++ b/dependency-reduced-pom.xml
@@ -52,7 +52,7 @@
tools
1.8
system
- ${JAVA_HOME}/lib/tools.jar
+ ${project.basedir}/lib/tools.jar
diff --git a/jbs-client/README.md b/jbs-client/README.md
new file mode 100644
index 0000000..5968eb1
--- /dev/null
+++ b/jbs-client/README.md
@@ -0,0 +1,18 @@
+# A tui for JVMByteSwapperTool
+Linux/MacOS supported
+
+Use the source code
+```bash
+$ go mod tidy
+$ go run . [options]
+```
+Or use the binary files in the release package (Only linux binary is provided, for other platforms, built by yourself)
+```bash
+$ ./jbs-client [options]
+```
+## options
+```
+--host localhost
+--http_port 8000
+--ws_port 18000
+```
\ No newline at end of file
diff --git a/jbs-client/go.mod b/jbs-client/go.mod
new file mode 100644
index 0000000..d416f38
--- /dev/null
+++ b/jbs-client/go.mod
@@ -0,0 +1,32 @@
+module github.com/sunwu51/jbs/client
+
+go 1.18
+
+require (
+ github.com/charmbracelet/bubbles v0.18.0
+ github.com/charmbracelet/bubbletea v0.25.0
+ github.com/charmbracelet/lipgloss v0.9.1
+ github.com/gorilla/websocket v1.5.1
+ github.com/thoas/go-funk v0.9.3
+)
+
+require (
+ github.com/atotto/clipboard v0.1.4 // indirect
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+ github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+ github.com/mattn/go-isatty v0.0.18 // indirect
+ github.com/mattn/go-localereader v0.0.1 // indirect
+ github.com/mattn/go-runewidth v0.0.15 // indirect
+ github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
+ github.com/muesli/cancelreader v0.2.2 // indirect
+ github.com/muesli/reflow v0.3.0 // indirect
+ github.com/muesli/termenv v0.15.2 // indirect
+ github.com/rivo/uniseg v0.4.6 // indirect
+ github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect
+ golang.org/x/net v0.17.0 // indirect
+ golang.org/x/sync v0.1.0 // indirect
+ golang.org/x/sys v0.13.0 // indirect
+ golang.org/x/term v0.13.0 // indirect
+ golang.org/x/text v0.13.0 // indirect
+)
diff --git a/jbs-client/go.sum b/jbs-client/go.sum
new file mode 100644
index 0000000..b2f72bc
--- /dev/null
+++ b/jbs-client/go.sum
@@ -0,0 +1,62 @@
+github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
+github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
+github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
+github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
+github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
+github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
+github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
+github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
+github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
+github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
+github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
+github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
+github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
+github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
+github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
+github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
+github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
+github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
+github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
+github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y=
+github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/thoas/go-funk v0.9.3 h1:7+nAEx3kn5ZJcnDm2Bh23N2yOtweO14bi//dvRtgLpw=
+github.com/thoas/go-funk v0.9.3/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
+golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
+golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/jbs-client/main.go b/jbs-client/main.go
new file mode 100644
index 0000000..117501a
--- /dev/null
+++ b/jbs-client/main.go
@@ -0,0 +1,76 @@
+package main
+
+import (
+ "flag"
+
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sunwu51/jbs/client/request"
+ "github.com/sunwu51/jbs/client/ui"
+)
+
+type Model struct {
+ state int
+ width int
+ height int
+ inputContainer ui.InputContainer
+ logContainer ui.LogContainer
+}
+
+func (m Model) Init() tea.Cmd {
+ return nil
+}
+
+func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch x := msg.(type) {
+ case tea.KeyMsg:
+ if x.Type == tea.KeyCtrlC {
+ return m, tea.Quit
+ }
+ case tea.WindowSizeMsg:
+ m.width, m.height = x.Width, x.Height
+ ready := 1
+ m.state = ready
+ }
+ i, cmd1 := m.inputContainer.Update(msg)
+ l, cmd2 := m.logContainer.Update(msg)
+ m.inputContainer = i
+ m.logContainer = l
+ return m, tea.Batch(cmd1, cmd2)
+}
+
+func (m Model) View() string {
+ if m.width < 200 || m.height < 35 {
+ return "Window need to larger than 200x35"
+ }
+ initializing := 0
+ if m.state == initializing {
+ return "Initializing..."
+ }
+ iv := lipgloss.NewStyle().Width(m.width / 2).Render(
+ m.inputContainer.View())
+ lv := m.logContainer.View()
+ return lipgloss.JoinHorizontal(lipgloss.Top, iv, lv)
+}
+
+func main() {
+ h := flag.String("host", "localhost", "server host")
+ port1 := flag.Int("http_port", 8000, "http port")
+ port2 := flag.Int("ws_port", 18000, "ws port")
+ flag.Parse()
+ request.Host = *h
+ request.HttpPort = *port1
+ request.WsPort = *port2
+ p := tea.NewProgram(Model{
+ inputContainer: ui.NewInputContainer(),
+ logContainer: ui.NewLogContainer(),
+ })
+ updateMsgChan := make(chan request.AppendLogMsg)
+ go request.ConnectWebSocket(updateMsgChan)
+ go func() {
+ for msg := range updateMsgChan {
+ p.Send(msg)
+ }
+ }()
+ p.Run()
+}
diff --git a/jbs-client/request/ws.go b/jbs-client/request/ws.go
new file mode 100644
index 0000000..1714c99
--- /dev/null
+++ b/jbs-client/request/ws.go
@@ -0,0 +1,60 @@
+package request
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "time"
+
+ "github.com/gorilla/websocket"
+)
+
+type AppendLogMsg string
+
+var ws *websocket.Conn
+var Host string
+var WsPort int
+var HttpPort int
+
+func ConnectWebSocket(updateMsgChan chan<- AppendLogMsg) {
+ dial := websocket.Dialer{}
+ c, _, err := dial.Dial(fmt.Sprintf("ws://%s:%d", Host, WsPort), nil)
+ ws = c
+ if err != nil {
+ log.Panic("dial:", err)
+ }
+ defer c.Close()
+
+ for {
+ _, message, err := c.ReadMessage()
+ if err != nil {
+ log.Fatal("read:", err)
+ break
+ }
+ j := make(map[string]string)
+ json.Unmarshal(message, &j)
+
+ updateMsgChan <- AppendLogMsg(time.Now().Format("[2006-01-02 15:04:05]") + " " + j["content"])
+ }
+}
+
+func SendMessage(msg string) error {
+ return ws.WriteMessage(websocket.TextMessage, []byte(msg))
+}
+
+func Reset() string {
+ url := fmt.Sprintf("http://%s:%d/reset", Host, HttpPort)
+ resp, err := http.Get(url)
+ if err != nil {
+ log.Panic(err)
+ }
+ defer resp.Body.Close()
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ log.Panic(err)
+ }
+
+ return string(body)
+}
diff --git a/jbs-client/ui/constants.go b/jbs-client/ui/constants.go
new file mode 100644
index 0000000..9fc29e0
--- /dev/null
+++ b/jbs-client/ui/constants.go
@@ -0,0 +1,193 @@
+package ui
+
+import (
+ "encoding/json"
+ "math/rand"
+ "strings"
+ "time"
+
+ "github.com/thoas/go-funk"
+)
+
+const (
+ textinputType = 0
+ textareaType = 1
+)
+
+var (
+ menu = make([]Function, 0)
+)
+
+type ParamChecker interface {
+ Check(string) bool
+}
+
+type Function struct {
+ Name string
+ Params []struct {
+ Name string
+ InputType int
+ Checker func(string) bool
+ Value string
+ }
+ ToJSON func([]string) string
+}
+
+func ClassAndMethodChecker(s string) bool { return len(strings.Split(s, "#")) == 2 }
+
+func CommonMap() map[string]interface{} {
+ m := make(map[string]interface{})
+ m["id"] = randomString(4)
+ m["timestamp"] = time.Now().UnixMilli()
+ return m
+}
+
+func WatchToJSON(params []string) string {
+ m := CommonMap()
+ m["type"] = "WATCH"
+ m["signature"] = params[0]
+ str, _ := json.Marshal(m)
+ return string(str)
+}
+
+func OuterWatchToJSON(params []string) string {
+ m := CommonMap()
+ m["type"] = "WATCH"
+ m["signature"] = params[0]
+ m["innerSignature"] = params[1]
+ str, _ := json.Marshal(m)
+ return string(str)
+}
+
+func TraceToJSON(params []string) string {
+ m := CommonMap()
+ m["type"] = "TRACE"
+ m["signature"] = params[0]
+ str, _ := json.Marshal(m)
+ return string(str)
+}
+
+func ChangeBodyToJSON(params []string) string {
+ m := CommonMap()
+ m["type"] = "CHANGE_BODY"
+ m["className"] = strings.Split(params[0], "#")[0]
+ m["method"] = strings.Split(params[0], "#")[1]
+ m["paramTypes"] = funk.Map(params[1], func(s string) string {
+ return strings.TrimSpace(s)
+ }).([]string)
+ m["body"] = params[2]
+ str, _ := json.Marshal(m)
+ return string(str)
+}
+
+func ChangeResultToJSON(params []string) string {
+ m := CommonMap()
+ m["type"] = "CHANGE_BODY"
+ m["className"] = strings.Split(params[0], "#")[0]
+ m["method"] = strings.Split(params[0], "#")[1]
+ m["paramTypes"] = funk.Map(params[1], func(s string) string {
+ return strings.TrimSpace(s)
+ }).([]string)
+ m["innerClassName"] = strings.Split(params[2], "#")[0]
+ m["innerMethod"] = strings.Split(params[2], "#")[1]
+ m["body"] = params[3]
+ str, _ := json.Marshal(m)
+ return string(str)
+}
+
+func ExecToJSON(params []string) string {
+ m := CommonMap()
+ m["body"] = params[0]
+ m["type"] = "EXEC"
+ str, _ := json.Marshal(m)
+ return string(str)
+}
+
+func init() {
+ rand.Seed(time.Now().UnixNano())
+ watch := Function{"Watch", []struct {
+ Name string
+ InputType int
+ Checker func(s string) bool
+ Value string
+ }{
+ {"ClassName#MethodName", 0, ClassAndMethodChecker, ""},
+ }, WatchToJSON}
+
+ outerWatch := Function{"OuterWatch", []struct {
+ Name string
+ InputType int
+ Checker func(s string) bool
+ Value string
+ }{
+ {"ClassName#MethodName", 0, ClassAndMethodChecker, ""},
+ {"InnerClassName#InnerMethodName", 0, ClassAndMethodChecker, ""},
+ }, OuterWatchToJSON}
+
+ trace := Function{"Trace", []struct {
+ Name string
+ InputType int
+ Checker func(s string) bool
+ Value string
+ }{
+ {"ClassName#MethodName", 0, ClassAndMethodChecker, ""},
+ }, TraceToJSON}
+
+ changeBody := Function{"ChangeBody", []struct {
+ Name string
+ InputType int
+ Checker func(s string) bool
+ Value string
+ }{
+ {"ClassName#MethodName", 0, ClassAndMethodChecker, ""},
+ {"ParamTypes", 0, func(s string) bool { return true }, ""},
+ {"Body", 1, func(s string) bool { return true }, ""},
+ }, ChangeBodyToJSON}
+
+ changeResult := Function{"ChangeResult", []struct {
+ Name string
+ InputType int
+ Checker func(s string) bool
+ Value string
+ }{
+ {"ClassName#MethodName", 0, ClassAndMethodChecker, ""},
+ {"ParamTypes", 0, func(s string) bool { return true }, ""},
+ {"InnerClassName#InnerMethodName", 0, ClassAndMethodChecker, ""},
+ {"Body", 1, func(s string) bool { return true }, ""},
+ }, ChangeResultToJSON}
+
+ exec := Function{"Exec", []struct {
+ Name string
+ InputType int
+ Checker func(s string) bool
+ Value string
+ }{
+ {"Body", 1, func(s string) bool { return true }, `{
+ try {
+ w.Global.info(w.Global.ognl("#root", ctx));
+ } catch(Exception e) {
+ w.Global.info(e.toString());
+ }
+}`},
+ }, ExecToJSON}
+
+ reset := Function{"Reset", []struct {
+ Name string
+ InputType int
+ Checker func(s string) bool
+ Value string
+ }{}, func(s []string) string { return "{}" }}
+
+ menu = []Function{watch, outerWatch, trace, changeBody, changeResult, exec, reset}
+
+}
+
+const letterNumberBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+
+func randomString(n int) string {
+ b := make([]byte, n)
+ for i := range b {
+ b[i] = letterNumberBytes[rand.Intn(len(letterNumberBytes))]
+ }
+ return string(b)
+}
diff --git a/jbs-client/ui/input_container.go b/jbs-client/ui/input_container.go
new file mode 100644
index 0000000..12ab6cf
--- /dev/null
+++ b/jbs-client/ui/input_container.go
@@ -0,0 +1,268 @@
+package ui
+
+import (
+ "fmt"
+ "io"
+ "strings"
+
+ "github.com/charmbracelet/bubbles/list"
+ "github.com/charmbracelet/bubbles/textarea"
+ "github.com/charmbracelet/bubbles/textinput"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sunwu51/jbs/client/request"
+)
+
+var (
+ focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
+ blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
+ focusedButton = focusedStyle.Render("[ Submit ]")
+ blurredButton = blurredStyle.Render("[ Submit ]")
+)
+
+type InputContainer struct {
+ chooseMenu list.Model
+ inputMenu inputs
+ level int
+ width int
+}
+
+type inputs struct {
+ focusIndex int
+ labels []string
+ inputs []inputModel
+}
+
+type inputModel interface {
+ Focus() tea.Cmd
+ Blur()
+ View() string
+ Value() string
+}
+type listItem string
+
+type listItemDelegate struct{}
+
+type chooseCursorMsg int
+type gotoMainMenu struct{}
+
+func (i listItem) FilterValue() string { return "" }
+
+func (d listItemDelegate) Height() int { return 1 }
+func (d listItemDelegate) Spacing() int { return 0 }
+func (d listItemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil }
+func (d listItemDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) {
+ i, ok := item.(listItem)
+ if !ok {
+ return
+ }
+ str := " " + fmt.Sprintf("%d. %s", index+1, i)
+ if index == m.Index() {
+ str = focusedStyle.Copy().Bold(true).
+ Render("> " + fmt.Sprintf("%d. %s", index+1, i))
+ }
+ fmt.Fprint(w, str)
+}
+
+// ========inputs: a custom ui component with multi inputs and labels
+func (m inputs) Init() tea.Cmd {
+ return func() tea.Msg {
+ return chooseCursorMsg(0)
+ }
+}
+
+func (m inputs) Update(msg tea.Msg) (inputs, tea.Cmd) {
+ cmds := make([]tea.Cmd, len(m.inputs))
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch msg.String() {
+ case "esc":
+ return m, func() tea.Msg { return gotoMainMenu{} }
+ case "tab", "shift+tab":
+ if msg.String() == "tab" {
+ m.focusIndex = (m.focusIndex + 1) % (len(m.inputs) + 1)
+ } else {
+ m.focusIndex = (m.focusIndex - 1) % (len(m.inputs) + 1)
+ }
+ for i, inp := range m.inputs {
+ if i == m.focusIndex {
+ cmds[i] = inp.Focus()
+ m.inputs[i] = inp
+ } else {
+ inp.Blur()
+ m.inputs[i] = inp
+ }
+ }
+ return m, tea.Batch(cmds...)
+ }
+ }
+ return m, m.updateInputs(msg)
+}
+
+func (m *inputs) updateInputs(msg tea.Msg) tea.Cmd {
+ cmds := make([]tea.Cmd, len(m.inputs))
+ for i := range m.inputs {
+ inp := m.inputs[i]
+ if _, ok := inp.(*textinput.Model); ok {
+ _i, _c := inp.(*textinput.Model).Update(msg)
+ inp = &_i
+ cmds[i] = _c
+
+ } else {
+ _i, _c := inp.(*textarea.Model).Update(msg)
+ inp = &_i
+ cmds[i] = _c
+ }
+ m.inputs[i] = inp
+ }
+ return tea.Batch(cmds...)
+}
+
+func (m inputs) View() string {
+ var b strings.Builder
+ for i := range m.inputs {
+ b.WriteString(m.labels[i] + "\n")
+ if i == m.focusIndex {
+ m.inputs[i].Focus()
+ if _, ok := m.inputs[i].(*textinput.Model); ok {
+ b.WriteString(focusedStyle.Render(m.inputs[i].View()))
+ } else {
+ area := m.inputs[i].(*textarea.Model)
+ area.FocusedStyle = textarea.Style{
+ CursorLine: focusedStyle,
+ Text: focusedStyle,
+ LineNumber: focusedStyle,
+ }
+ b.WriteString(m.inputs[i].View())
+ }
+ } else {
+ m.inputs[i].Blur()
+ b.WriteString(m.inputs[i].View())
+ }
+ b.WriteRune('\n')
+ }
+
+ button := &blurredButton
+ if m.focusIndex == len(m.inputs) {
+ button = &focusedButton
+ }
+ fmt.Fprintf(&b, "\n\n%s\n", *button)
+ b.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#0F0")).Render("press esc go back"))
+ return b.String()
+}
+
+// ======InputContainer: a custom ui component combined by 2 components:
+//
+// a choose list list.Model and a inputs, when level=0 choose list is active and showed
+// when level=1 the inputs is active and choose list hides
+func (m InputContainer) Init() tea.Cmd {
+ return nil
+}
+
+func (m InputContainer) Update(msg tea.Msg) (InputContainer, tea.Cmd) {
+ var cmd tea.Cmd
+ if m.level == 0 {
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ m.width = msg.Width
+ case tea.KeyMsg:
+ switch msg.Type {
+ case tea.KeyTab:
+ if m.chooseMenu.Cursor() == len(menu)-1 {
+ m.chooseMenu.Select(0)
+ } else {
+ m.chooseMenu.CursorDown()
+ }
+ case tea.KeyShiftTab:
+ if m.chooseMenu.Cursor() == 0 {
+ m.chooseMenu.Select(len(menu) - 1)
+ } else {
+ m.chooseMenu.CursorUp()
+ }
+ case tea.KeyEnter:
+ m.level = 1
+ m.inputMenu.focusIndex = 0
+ params := menu[int(m.chooseMenu.Cursor())].Params
+ inputs := make([]inputModel, len(params))
+ labels := make([]string, len(params))
+ for i := range inputs {
+ labels[i] = params[i].Name
+ if params[i].InputType == textareaType {
+ t := textarea.New()
+ t.SetWidth(m.width/2 - 1)
+ t.SetHeight(25)
+ if i == 0 {
+ t.Focus()
+ }
+ t.SetValue(params[i].Value)
+ inputs[i] = &t
+ } else if params[i].InputType == textinputType {
+ t := textinput.New()
+ if i == 0 {
+ t.Focus()
+ }
+ t.SetValue(params[i].Value)
+ inputs[i] = &t
+ }
+ }
+ m.inputMenu.inputs = inputs
+ m.inputMenu.labels = labels
+ return m, nil
+
+ }
+ }
+ return m, func() tea.Msg {
+ return chooseCursorMsg(m.chooseMenu.Cursor())
+ }
+ } else {
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ // submit enter
+ if m.inputMenu.focusIndex == len(m.inputMenu.inputs) && msg.Type == tea.KeyEnter {
+ if menu[m.chooseMenu.Cursor()].Name == "Reset" {
+ res := request.Reset()
+ return m, tea.Batch(func() tea.Msg { return gotoMainMenu{} }, func() tea.Msg { return request.AppendLogMsg("reset:" + res) })
+ }
+ vals := []string{}
+ for _, inp := range m.inputMenu.inputs {
+ vals = append(vals, inp.Value())
+ }
+ for i, p := range menu[m.chooseMenu.Cursor()].Params {
+ if !p.Checker(m.inputMenu.inputs[i].Value()) {
+ return m, func() tea.Msg { return request.AppendLogMsg("Param Invalid") }
+ }
+ }
+ request.SendMessage(menu[m.chooseMenu.Cursor()].ToJSON(vals))
+ }
+ case gotoMainMenu:
+ m.level = 0
+ }
+ m.inputMenu, cmd = m.inputMenu.Update(msg)
+ }
+
+ return m, cmd
+}
+
+func (m InputContainer) View() string {
+ if m.level == 0 {
+ return m.chooseMenu.View()
+ }
+ return m.inputMenu.View()
+}
+
+func NewInputContainer() InputContainer {
+ items := make([]list.Item, 0)
+ for _, k := range menu {
+ items = append(items, listItem(k.Name))
+ }
+ chooseMenu := list.New(items, listItemDelegate{}, 30, 14)
+ chooseMenu.Title = "Input the action?"
+ chooseMenu.Styles.Title = focusedStyle
+ chooseMenu.SetShowHelp(false)
+ chooseMenu.SetShowStatusBar(false)
+ chooseMenu.SetFilteringEnabled(false)
+ return InputContainer{
+ level: 0,
+ chooseMenu: chooseMenu,
+ }
+}
diff --git a/jbs-client/ui/log_container.go b/jbs-client/ui/log_container.go
new file mode 100644
index 0000000..2e1d204
--- /dev/null
+++ b/jbs-client/ui/log_container.go
@@ -0,0 +1,58 @@
+package ui
+
+import (
+ "strings"
+
+ "github.com/charmbracelet/bubbles/textarea"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/sunwu51/jbs/client/request"
+)
+
+const logMaxLength = 200
+
+type LogContainer struct {
+ messages []string
+ text textarea.Model
+}
+
+func (m LogContainer) Init() tea.Cmd {
+ return nil
+}
+
+func (m LogContainer) Update(msg tea.Msg) (LogContainer, tea.Cmd) {
+ switch msg := msg.(type) {
+ case request.AppendLogMsg:
+ m.messages = append([]string{string(msg)}, m.messages...)
+ str := strings.Join(m.messages, "\n")
+ if len(m.messages) > logMaxLength {
+ m.messages = m.messages[0:logMaxLength]
+ }
+ m.text.SetValue(str)
+ case tea.WindowSizeMsg:
+ m.text.SetWidth(msg.Width/2 - 10)
+ }
+ return m, nil
+}
+
+func (m LogContainer) View() string {
+ st := lipgloss.NewStyle().
+ Border(lipgloss.RoundedBorder()).
+ BorderForeground(lipgloss.Color("#26f7ce")).
+ BorderBackground(lipgloss.Color("#26f7ce")).
+ Padding(1)
+ return st.Render(m.text.View())
+}
+
+func NewLogContainer() LogContainer {
+ text := textarea.New()
+ text.SetHeight(34)
+ text.ShowLineNumbers = false
+ text.Prompt = ""
+ text.Blur()
+ text.CharLimit = -1
+ return LogContainer{
+ messages: []string{},
+ text: text,
+ }
+}
diff --git a/jexer-client/README.md b/jexer-client/README.md
new file mode 100644
index 0000000..f1f4d13
--- /dev/null
+++ b/jexer-client/README.md
@@ -0,0 +1,2 @@
+# tui client based on jexer
+Deprecated now, please use jbs-client written by golang
\ No newline at end of file
diff --git a/jexer-client/dependency-reduced-pom.xml b/jexer-client/dependency-reduced-pom.xml
new file mode 100644
index 0000000..4ba2c7b
--- /dev/null
+++ b/jexer-client/dependency-reduced-pom.xml
@@ -0,0 +1,36 @@
+
+
+ 4.0.0
+ w
+ jbs-client
+ 1.0-SNAPSHOT
+
+
+
+ maven-shade-plugin
+ 3.5.0
+
+
+ package
+
+ shade
+
+
+
+
+
+
+
+ w.client.Main
+
+
+
+
+
+
+
+
+ 8
+ 8
+
+
diff --git a/jexer-client/pom.xml b/jexer-client/pom.xml
new file mode 100644
index 0000000..998e63b
--- /dev/null
+++ b/jexer-client/pom.xml
@@ -0,0 +1,61 @@
+
+
+ 4.0.0
+
+ w
+ jbs-client
+ 1.0-SNAPSHOT
+
+
+ 8
+ 8
+
+
+
+
+ com.gitlab.klamonte
+ jexer
+ 1.6.0
+
+
+ org.java-websocket
+ Java-WebSocket
+ 1.5.6
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.13.5
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.0
+
+
+
+
+ w.client.Main
+
+
+
+
+
+
+ package
+
+ shade
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/jexer-client/src/main/java/w/client/App.java b/jexer-client/src/main/java/w/client/App.java
new file mode 100644
index 0000000..b9a553d
--- /dev/null
+++ b/jexer-client/src/main/java/w/client/App.java
@@ -0,0 +1,91 @@
+package w.client;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.java_websocket.client.WebSocketClient;
+import org.java_websocket.handshake.ServerHandshake;
+
+import java.net.URI;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+public class App extends WebSocketClient {
+
+ ObjectMapper mapper = new ObjectMapper();
+
+ Tui tui;
+
+ public App(URI serverUri) {
+ super(serverUri);
+ this.setConnectionLostTimeout(0);
+ }
+
+ @Override
+ public void onOpen(ServerHandshake handshakedata) {
+ System.out.println("Opened new connection to " + getURI());
+ System.out.println("Will open the tui");
+ if (tui == null) {
+ CompletableFuture.runAsync(() -> {
+ try {
+ Ws ws = new Ws() {
+ @Override
+ public void send(Map map) {
+ try {
+ String json = mapper.writeValueAsString(map);
+ sendMessage(json);
+ } catch (JsonProcessingException e) {
+ }
+ }
+
+ @Override
+ public void close() {
+ App.this.close();
+ }
+ };
+ tui = new Tui(ws);
+ tui.run();
+ } catch (Exception e) {
+ System.err.println("Open tui error!");
+ e.printStackTrace();
+ }
+ });
+
+ }
+ }
+
+ @Override
+ public void onMessage(String message) {
+ if (tui != null) {
+ Map json;
+ try {
+ json = mapper.readValue(message, new TypeReference
-
com.sun
tools
1.8
system
- ${JAVA_HOME}/lib/tools.jar
+ ${project.basedir}/lib/tools.jar
org.nanohttpd
diff --git a/src/main/java/w/App.java b/src/main/java/w/App.java
index ae017c7..fa98274 100644
--- a/src/main/java/w/App.java
+++ b/src/main/java/w/App.java
@@ -1,13 +1,16 @@
package w;
-import java.io.IOException;
+import java.io.*;
import java.lang.instrument.Instrumentation;
-import java.lang.management.ManagementFactory;
-import java.lang.management.RuntimeMXBean;
import java.lang.reflect.InvocationTargetException;
-import java.util.List;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
import javassist.*;
@@ -25,6 +28,17 @@ public static void agentmain(String arg, Instrumentation instrumentation) throws
Global.debug("Already attached before");
return;
}
+ if (arg != null && arg.length() > 0) {
+ String[] items = arg.split("&");
+ for (String item : items) {
+ String[] kv = item.split("=");
+ if (kv.length == 2) {
+ if (System.getProperty(kv[0]) == null) {
+ System.setProperty(kv[0], kv[1]);
+ }
+ }
+ }
+ }
Global.instrumentation = instrumentation;
Global.fillLoadedClasses();
@@ -81,6 +95,4 @@ private static void schedule() {
Executors.newScheduledThreadPool(1)
.scheduleWithFixedDelay(Global::fillLoadedClasses, 5, 60, TimeUnit.SECONDS);
}
-
-
}
diff --git a/src/main/java/w/Attach.java b/src/main/java/w/Attach.java
index 327d90a..07ddf24 100644
--- a/src/main/java/w/Attach.java
+++ b/src/main/java/w/Attach.java
@@ -1,12 +1,15 @@
package w;
-import com.sun.tools.attach.*;
+import com.sun.tools.attach.VirtualMachine;
+import com.sun.tools.attach.VirtualMachineDescriptor;
+import w.util.WClassLoader;
import java.io.File;
-import java.io.IOException;
-import java.net.URISyntaxException;
+import java.lang.reflect.Method;
import java.net.URL;
import java.nio.file.Paths;
+import java.security.CodeSource;
+import java.security.ProtectionDomain;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
@@ -17,8 +20,30 @@
* @date 2023/11/26 13:07
*/
public class Attach {
-
- public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, URISyntaxException {
+ public static void main(String[] args) throws Exception {
+ if (!Attach.class.getClassLoader().toString().startsWith(WClassLoader.namePrefix)) {
+ String jdkVersion = System.getProperty("java.version");
+ if (jdkVersion.startsWith("1.")) {
+ if (jdkVersion.startsWith("1.8")) {
+ try {
+ // custom class loader to load current jar and tools.jar
+ WClassLoader customClassLoader = new WClassLoader(
+ new URL[]{toolsJarUrl(), currentUrl()},
+ ClassLoader.getSystemClassLoader().getParent()
+ );
+ Class> mainClass = Class.forName("w.Attach", true, customClassLoader);
+ Method mainMethod = mainClass.getMethod("main", String[].class);
+ mainMethod.invoke(null, (Object) args);
+ return;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ } else {
+ Global.error(jdkVersion + " is not supported");
+ return;
+ }
+ }
+ }
// Get the jvm process PID from args[0] or manual input
// And get the spring http port from manual input
@@ -60,13 +85,36 @@ public static void main(String[] args) throws IOException, AttachNotSupportedExc
URL jarUrl = Attach.class.getProtectionDomain().getCodeSource().getLocation();
String curJarPath = Paths.get(jarUrl.toURI()).toString();
try {
- jvm.loadAgent(curJarPath);
+ StringBuilder arg = new StringBuilder();
+ System.getProperties().forEach((k, v) -> {
+ if (k.toString().startsWith("w_") && k.toString().length() > 2) {
+ arg.append(k.toString().substring(2)).append("=").append(v.toString()).append("&");
+ }
+ });
+
+ jvm.loadAgent(curJarPath, arg.toString());
jvm.detach();
- } catch (AgentLoadException e) {
+ } catch (Exception e) {
if (!Objects.equals(e.getMessage(), "0")) {
throw e;
}
}
System.out.println("============Attach finish");
}
+
+ private static URL toolsJarUrl() throws Exception {
+ String javaHome = System.getProperty("java.home");
+ File toolsJarFile = new File(javaHome, "../lib/tools.jar");
+ if (!toolsJarFile.exists()) {
+ throw new Exception("tools.jar not found at: " + toolsJarFile.getPath());
+ }
+ URL toolsJarUrl = toolsJarFile.toURI().toURL();
+ return toolsJarUrl;
+ }
+
+ private static URL currentUrl() throws Exception {
+ ProtectionDomain domain = Attach.class.getProtectionDomain();
+ CodeSource codeSource = domain.getCodeSource();
+ return codeSource.getLocation();
+ }
}
diff --git a/src/main/java/w/Global.java b/src/main/java/w/Global.java
index d6634df..b22c4ff 100644
--- a/src/main/java/w/Global.java
+++ b/src/main/java/w/Global.java
@@ -17,9 +17,14 @@
import java.io.StringWriter;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Paths;
import java.util.*;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
@@ -59,6 +64,11 @@ public class Global {
*/
public static Map>> activeTransformers = new ConcurrentHashMap<>();
+ /**
+ * Transformer hitCounter, for watch trace the transformer effects 50 times at most by default.
+ * Change the default value by environment variable $HIT_COUNT
+ */
+ public static Map hitCounter = new ConcurrentHashMap<>();
/**
* OgnlContext inited at static code block
@@ -225,10 +235,10 @@ public static synchronized void addTransformer(BaseClassTransformer transformer)
* @throws UnmodifiableClassException
*/
public static synchronized void addActiveTransformer(Class> c, BaseClassTransformer transformer) throws UnmodifiableClassException {
- instrumentation.retransformClasses(c);
String className = c.getName();
String classLoader = c.getClassLoader().toString();
activeTransformers.computeIfAbsent(className, k->new HashMap<>()).computeIfAbsent(classLoader, k->new ArrayList<>()).add(transformer);
+ instrumentation.retransformClasses(c);
}
@@ -237,6 +247,7 @@ public static synchronized void addActiveTransformer(Class> c, BaseClassTransf
* @param uuid
*/
public static synchronized void deleteTransformer(UUID uuid) {
+ debug("Deleting transformer " + uuid);
Set delClass = new HashSet<>();
transformers.removeIf(it -> {
if (it.getUuid().equals(uuid)) {
@@ -273,6 +284,7 @@ public static synchronized void deleteTransformer(UUID uuid) {
for (String aClass : delClass) {
activeTransformers.remove(aClass);
}
+ debug("Deleted transformer " + uuid);
}
/**
@@ -336,7 +348,7 @@ private static void log(int level, String content) {
break;
case 2:
default:
- log.log(Level.SEVERE, "[error]" + content);
+ log.log(Level.WARNING, "[error]" + content);
}
send(level, content);
}
@@ -361,6 +373,25 @@ private static synchronized void send(int level, String content) {
}
}
+ public static void checkCountAndUnload(String uuid) {
+ if (hitCounter.computeIfAbsent(uuid, k -> new AtomicInteger()).incrementAndGet() >= getMaxHit()) {
+ info("Watch or trace hit counter exceeded maximum, deleted");
+ deleteTransformer(UUID.fromString(uuid));
+ hitCounter.remove(uuid);
+ }
+ }
+
+ public static int getMaxHit() {
+ try {
+ return Integer.parseInt(System.getProperty("maxHit"));
+ } catch (Exception e) {
+ return 100;
+ }
+ }
+ public static List readFile(String path) throws IOException {
+ return Files.readAllLines(Paths.get(path));
+ }
+
public synchronized static void fillLoadedClasses() {
int count = 0;
long start = System.currentTimeMillis();
@@ -373,6 +404,6 @@ public synchronized static void fillLoadedClasses() {
}
}
- debug("fill loaded classes cost: " + (System.currentTimeMillis() - start) + "ms, class num:" + count);
+// debug("fill loaded classes cost: " + (System.currentTimeMillis() - start) + "ms, class num:" + count);
}
}
diff --git a/src/main/java/w/core/Swapper.java b/src/main/java/w/core/Swapper.java
index f259caa..7ff912e 100644
--- a/src/main/java/w/core/Swapper.java
+++ b/src/main/java/w/core/Swapper.java
@@ -50,7 +50,7 @@ public boolean swap(Message message) {
Set> classes = Global.allLoadedClasses.getOrDefault(transformer.getClassName(), new HashSet<>());
- boolean exists = false;
+ boolean classExists = false;
for (Class> aClass : classes) {
if (aClass.isInterface() || Modifier.isAbstract(aClass.getModifiers())) {
Set candidates = new HashSet<>();
@@ -60,10 +60,10 @@ public boolean swap(Message message) {
Global.error("!Error: Should use a simple pojo, but " + aClass.getName() + " is a Interface or Abstract class or something wired, \nmaybe you should use: " + candidates);
return false;
}
- exists = true;
+ classExists = true;
}
- if (!exists) {
+ if (!classExists) {
Global.error("Class not exist" + transformer.getClassName());
return false;
}
@@ -77,6 +77,7 @@ public boolean swap(Message message) {
Global.addActiveTransformer(aClass, transformer);
} catch (Throwable e) {
Global.error("re transformer error:", e);
+ Global.deleteTransformer(transformer.getUuid());
return false;
}
}
diff --git a/src/main/java/w/core/model/BaseClassTransformer.java b/src/main/java/w/core/model/BaseClassTransformer.java
index 5c1b832..6742aff 100644
--- a/src/main/java/w/core/model/BaseClassTransformer.java
+++ b/src/main/java/w/core/model/BaseClassTransformer.java
@@ -12,6 +12,7 @@
import java.security.ProtectionDomain;
import java.util.Objects;
import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
/**
* @author Frank
@@ -45,6 +46,8 @@ public byte[] transform(ClassLoader loader, String className, Class> classBein
return r;
} catch (Exception e) {
Global.error(className + " transformer " + uuid + " added fail -(′д`)-: ", e);
+ // async to delete, because current thread holds the class lock
+ CompletableFuture.runAsync(() -> Global.deleteTransformer(uuid));
}
}
return null;
diff --git a/src/main/java/w/core/model/ChangeBodyTransformer.java b/src/main/java/w/core/model/ChangeBodyTransformer.java
index 3fe135c..8fe166c 100644
--- a/src/main/java/w/core/model/ChangeBodyTransformer.java
+++ b/src/main/java/w/core/model/ChangeBodyTransformer.java
@@ -8,6 +8,7 @@
import w.web.message.ChangeBodyMessage;
import java.io.ByteArrayInputStream;
+import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@@ -37,14 +38,19 @@ public ChangeBodyTransformer(ChangeBodyMessage message) {
@Override
public byte[] transform(String className, byte[] origin) throws Exception {
CtClass ctClass = Global.classPool.makeClass(new ByteArrayInputStream(origin));
+ boolean effect = false;
for (CtMethod declaredMethod : ctClass.getDeclaredMethods()) {
if (Objects.equals(declaredMethod.getName(), method) &&
Arrays.equals(paramTypes.toArray(new String[0]),
Arrays.stream(declaredMethod.getParameterTypes()).map(CtClass::getName).toArray())
) {
declaredMethod.setBody(message.getBody());
+ effect = true;
}
}
+ if (!effect) {
+ throw new IllegalArgumentException("Class or Method not exist.");
+ }
byte[] result = ctClass.toBytecode();
ctClass.detach();
status = 1;
diff --git a/src/main/java/w/core/model/ChangeResultTransformer.java b/src/main/java/w/core/model/ChangeResultTransformer.java
index 80b4939..1baab17 100644
--- a/src/main/java/w/core/model/ChangeResultTransformer.java
+++ b/src/main/java/w/core/model/ChangeResultTransformer.java
@@ -48,6 +48,7 @@ public ChangeResultTransformer(ChangeResultMessage message) {
@Override
public byte[] transform(String className, byte[] origin) throws Exception {
CtClass ctClass = Global.classPool.makeClass(new ByteArrayInputStream(origin));
+ boolean effect = false;
for (CtMethod declaredMethod : ctClass.getDeclaredMethods()) {
if (Objects.equals(declaredMethod.getName(), method) &&
Arrays.equals(paramTypes.toArray(new String[0]),
@@ -62,8 +63,12 @@ public void edit(MethodCall m) throws CannotCompileException {
}
}
});
+ effect = true;
}
}
+ if (!effect) {
+ throw new IllegalArgumentException("Class or Method not exist.");
+ }
byte[] result = ctClass.toBytecode();
ctClass.detach();
status = 1;
diff --git a/src/main/java/w/core/model/OuterWatchTransformer.java b/src/main/java/w/core/model/OuterWatchTransformer.java
index 3cf0b96..ed5c35b 100644
--- a/src/main/java/w/core/model/OuterWatchTransformer.java
+++ b/src/main/java/w/core/model/OuterWatchTransformer.java
@@ -1,8 +1,6 @@
package w.core.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import fi.iki.elonen.NanoHTTPD;
import javassist.*;
import javassist.expr.ExprEditor;
import javassist.expr.MethodCall;
@@ -45,11 +43,16 @@ public OuterWatchTransformer(OuterWatchMessage watchMessage) {
@Override
public byte[] transform(String className, byte[] origin) throws Exception {
CtClass ctClass = Global.classPool.makeClass(new ByteArrayInputStream(origin));
+ boolean effect = false;
for (CtMethod declaredMethod : ctClass.getDeclaredMethods()) {
if (Objects.equals(declaredMethod.getName(), method)) {
addOuterWatchCodeToMethod(declaredMethod);
+ effect = true;
}
}
+ if (!effect) {
+ throw new IllegalArgumentException("Class or Method not exist.");
+ }
byte[] result = ctClass.toBytecode();
ctClass.detach();
status = 1;
@@ -62,6 +65,7 @@ public void edit(MethodCall m) throws CannotCompileException {
if (m.getMethodName().equals(innerMethod)) {
if (innerClassName.equals("*") || m.getClassName().equals(innerClassName)) {
String code = "{" +
+ "w.Global.checkCountAndUnload(\"" + uuid + "\");\n" +
"long start = System.currentTimeMillis();" +
"String req = null;" +
"String res = null;" +
diff --git a/src/main/java/w/core/model/TraceTransformer.java b/src/main/java/w/core/model/TraceTransformer.java
index 6b6e859..f0e3529 100644
--- a/src/main/java/w/core/model/TraceTransformer.java
+++ b/src/main/java/w/core/model/TraceTransformer.java
@@ -1,6 +1,7 @@
package w.core.model;
import java.io.ByteArrayInputStream;
+import java.lang.reflect.Method;
import java.util.Objects;
import javassist.CannotCompileException;
@@ -26,14 +27,19 @@ public TraceTransformer(TraceMessage traceMessage) {
this.traceId = traceMessage.getId();
}
- @Override
+ @Override
public byte[] transform(String className, byte[] origin) throws Exception {
CtClass ctClass = Global.classPool.makeClass(new ByteArrayInputStream(origin));
+ boolean effect = false;
for (CtMethod declaredMethod : ctClass.getDeclaredMethods()) {
if (Objects.equals(declaredMethod.getName(), method)) {
addTraceCodeToMethod(declaredMethod);
+ effect = true;
}
}
+ if (!effect) {
+ throw new IllegalArgumentException("Class or Method not exist.");
+ }
byte[] result = ctClass.toBytecode();
ctClass.detach();
status = 1;
@@ -57,6 +63,7 @@ public void edit(MethodCall m) throws CannotCompileException {
ctMethod.addLocalVariable("s", CtClass.longType);
ctMethod.addLocalVariable("cost", CtClass.longType);
ctMethod.insertBefore("s = System.currentTimeMillis();");
+ ctMethod.insertBefore("w.Global.checkCountAndUnload(\"" + uuid + "\");");
ctMethod.insertAfter("{" +
"cost = System.currentTimeMillis() - s;" +
"w.util.RequestUtils.fillCurThread(\"" + message.getId() + "\");\n" +
diff --git a/src/main/java/w/core/model/WatchTransformer.java b/src/main/java/w/core/model/WatchTransformer.java
index 72062c3..6547e67 100644
--- a/src/main/java/w/core/model/WatchTransformer.java
+++ b/src/main/java/w/core/model/WatchTransformer.java
@@ -7,9 +7,12 @@
import w.web.message.WatchMessage;
import java.io.ByteArrayInputStream;
+import java.lang.reflect.Method;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
/**
* @author Frank
@@ -35,11 +38,16 @@ public WatchTransformer(WatchMessage watchMessage) {
@Override
public byte[] transform(String className, byte[] origin) throws Exception {
CtClass ctClass = Global.classPool.makeClass(new ByteArrayInputStream(origin));
+ boolean effect = false;
for (CtMethod declaredMethod : ctClass.getDeclaredMethods()) {
if (Objects.equals(declaredMethod.getName(), method)) {
addWatchCodeToMethod(declaredMethod);
+ effect = true;
}
}
+ if (!effect) {
+ throw new IllegalArgumentException("Class or Method not exist.");
+ }
byte[] result = ctClass.toBytecode();
ctClass.detach();
status = 1;
@@ -53,6 +61,7 @@ private void addWatchCodeToMethod(CtMethod ctMethod) throws CannotCompileExcepti
ctMethod.addLocalVariable("req", Global.classPool.get("java.lang.String"));
ctMethod.addLocalVariable("res", Global.classPool.get("java.lang.String"));
ctMethod.insertBefore("startTime = System.currentTimeMillis();");
+ ctMethod.insertBefore("w.Global.checkCountAndUnload(\"" + uuid + "\");\n");
StringBuilder afterCode = new StringBuilder("{\n")
.append("endTime = System.currentTimeMillis();\n")
diff --git a/src/main/java/w/util/WClassLoader.java b/src/main/java/w/util/WClassLoader.java
new file mode 100644
index 0000000..443b0ac
--- /dev/null
+++ b/src/main/java/w/util/WClassLoader.java
@@ -0,0 +1,27 @@
+package w.util;
+
+import java.net.URL;
+import java.net.URLClassLoader;
+
+/**
+ * @author Frank
+ * @date 2024/4/4 17:09
+ */
+public class WClassLoader extends URLClassLoader {
+
+ public static String namePrefix = "WClassLoader";
+
+ public WClassLoader(URL[] urls, ClassLoader parent) {
+ super(urls, parent);
+ }
+
+ @Override
+ protected Class> findClass(String name) throws ClassNotFoundException {
+ return super.findClass(name);
+ }
+
+ @Override
+ public String toString() {
+ return namePrefix + super.toString();
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/nanohttpd/index.html b/src/main/resources/nanohttpd/index.html
index 3027f37..b52d89f 100644
--- a/src/main/resources/nanohttpd/index.html
+++ b/src/main/resources/nanohttpd/index.html
@@ -15,6 +15,7 @@
+