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>() { + }); + if (json.get("type").equals("LOG")) { + tui.showLog(json.get("content") + ""); + } + } catch (Exception e) { + } + + } + } + + @Override + public void onClose(int code, String reason, boolean remote) { + if (tui != null) { + tui.exit(); + } + System.out.println("Closed connection to " + getURI() + "; Code: " + code + " Reason: " + reason); + System.exit(1); + } + + @Override + public void onError(Exception ex) { + System.err.println(ex); + } + + public void sendMessage(String message) { + send(message); + } + +} \ No newline at end of file diff --git a/jexer-client/src/main/java/w/client/Main.java b/jexer-client/src/main/java/w/client/Main.java new file mode 100644 index 0000000..f455276 --- /dev/null +++ b/jexer-client/src/main/java/w/client/Main.java @@ -0,0 +1,15 @@ +package w.client; + +import java.net.URI; + +public class Main { + public static void main(String[] args) throws Exception { + int port = 18000; + String host = "localhost"; + if (args.length > 0) { + port = Integer.parseInt(args[1]); + } + URI uri = URI.create("ws://" + host + ":" + port); + new App(uri).connect(); + } +} diff --git a/jexer-client/src/main/java/w/client/Tui.java b/jexer-client/src/main/java/w/client/Tui.java new file mode 100644 index 0000000..cead5d1 --- /dev/null +++ b/jexer-client/src/main/java/w/client/Tui.java @@ -0,0 +1,306 @@ +package w.client; + +import jexer.TAction; +import jexer.TApplication; +import jexer.TEditorWidget; +import jexer.TField; +import jexer.TWindow; +import jexer.event.TMenuEvent; +import jexer.menu.TMenu; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.UUID; + +/** + * @author Frank + * @date 2024/3/16 11:30 + */ + +public class Tui extends TApplication { + TWindow twatch = genWatchWindow(); + + TWindow tchange = genChangeBodyWindow(); + + TWindow texec = genExecWindow(); + + TWindow trep = genReplaceClassWindow(); + + final TWindow tlog; + + final TEditorWidget logger; + + LinkedList logQueue = new LinkedList<>(); + + Map id2Win = new HashMap<>(); + + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-mm-dd HH:MM:ss"); + + Ws ws; + + public Tui(Ws ws) throws Exception { + super(BackendType.XTERM); + this.ws = ws; + TMenu menu = this.addMenu("&Menu"); + menu.addItem(1001, "watch"); + menu.addItem(1002, "changeBody"); + menu.addItem(1003, "exec"); + menu.addItem(1004, "replace"); + menu.addItem(1005, "log"); + + menu.addItem(9999, "exit"); + + Object[] o = genLogWindow(); + + tlog = (TWindow) o[0]; + logger = (TEditorWidget) o[1]; + this.tchange.hide(); + this.twatch.hide(); + this.trep.hide(); + id2Win.put(1001, twatch); + id2Win.put(1002, tchange); + id2Win.put(1003, texec); + id2Win.put(1004, trep); + id2Win.put(1005, tlog); + + } + + private Object[] genLogWindow() { + // 创建一个新的窗口 + TWindow window = new TWindow(this, "Log", 100, 0, 140, 40) { + @Override + public void onClose() { + this.hide(); + } + }; + + TEditorWidget logger = window.addEditor("", 1, 1, window.getWidth(), window.getHeight()); + return new Object[] { window, logger }; + } + + private TWindow genWatchWindow() { + + // 创建一个新的窗口 + TWindow window = new TWindow(this, "Watch", 0, 0, 100, 40, TWindow.HIDEONCLOSE); + // 添加标签和输入框 + window.addLabel("method signature", 1, 2); + TField methodInput = window.addField(18, 2, 77, false, ""); + + window.addButton("&Watch", 18, 4, new TAction() { + public void DO() { + if (methodInput.getText().split("#").length != 2) { + showLog("client error, method signature error"); + return; + } + SendEvent event = new SendEvent("WATCH"); + event.put("signature", methodInput.getText()); + handleSubmit(event); + } + }); + + window.addButton("&Trace", 32, 4, new TAction() { + public void DO() { + if (methodInput.getText().split("#").length != 2) { + showLog("client error, method signature error"); + return; + } + SendEvent event = new SendEvent("TRACE"); + event.put("signature", methodInput.getText()); + handleSubmit(event); + } + }); + + window.addLabel("outer method", 1, 6); + TField outerMethodInput = window.addField(18, 6, 77, false, ""); + window.addLabel("inner method", 1, 8); + TField innerMethodInput = window.addField(18, 8, 77, false, ""); + window.addButton("&OuterWatch", 18, 10, new TAction() { + public void DO() { + if (outerMethodInput.getText().split("#").length != 2 + || innerMethodInput.getText().split("#").length != 2) { + showLog("client error, method signature error"); + return; + } + SendEvent event = new SendEvent("OUTER_WATCH"); + event.put("signature", outerMethodInput.getText()); + event.put("innerSignature", innerMethodInput.getText()); + handleSubmit(event); + } + }); + return window; + } + + private TWindow genChangeBodyWindow() { + // 创建一个新的窗口 + TWindow window = new TWindow(this, "ChangeBody", 0, 0, 100, 40, TWindow.HIDEONCLOSE); + window.addLabel("method signature", 1, 2); + TField methodInput = window.addField(18, 2, 77, false, ""); + + window.addLabel("param list", 1, 4); + TField paramInput = window.addField(18, 4, 77, false, ""); + TEditorWidget edit = window.addEditor("{\r\n" + // + "\t// write some java code\r\n" + // + "}", 18, 6, 77, 10); + window.addButton("&ChangeBody", 18, 18, new TAction() { + public void DO() { + String[] arr = null; + if ((arr = methodInput.getText().split("#")).length != 2) { + showLog("client error, method signature error"); + return; + } + SendEvent event = new SendEvent("CHANGE_BODY"); + event.put("className", arr[0]); + event.put("method", arr[1]); + event.put("paramTypes", paramInput.getText()); + event.put("body", edit.getText()); + handleSubmit(event); + } + }); + + window.addLabel("outer method", 1, 20); + TField outer = window.addField(18, 20, 77, false, ""); + + window.addLabel("outer param list", 1, 22); + TField parm = window.addField(18, 22, 77, false, ""); + window.addLabel("inner methor", 1, 24); + TField inner = window.addField(18, 24, 77, false, ""); + TEditorWidget edit2 = window.addEditor("// $_ = \"new return value\";", 18, 26, 77, 10); + window.addButton("&ChangeResult", 18, 38, new TAction() { + public void DO() { + String[] arr = null, arr2 = null; + if ((arr = outer.getText().split("#")).length != 2 || (arr2 = inner.getText().split("#")).length != 2) { + showLog("client error, method signature error"); + return; + } + SendEvent event = new SendEvent("CHANGE_RESULT"); + event.put("className", arr[0]); + event.put("method", arr[1]); + event.put("paramTypes", parm.getText()); + event.put("innerClassName", arr2[0]); + event.put("innerMethod", arr2[1]); + + event.put("body", edit2.getText()); + handleSubmit(event); + } + }); + + return window; + } + + private TWindow genExecWindow() { + // 创建一个新的窗口 + TWindow window = new TWindow(this, "Exec", 0, 0, 100, 40, TWindow.HIDEONCLOSE); + TEditorWidget edit = window.addEditor("{\r\n" + // + " try {\r\n" + // + " // write some java code\r\n" + // + " // w.Global.info(w.Global.ognl(\"#root\", ctx));\r\n" + // + " } catch(Exception e) {\r\n" + // + " w.Global.info(e.toString());\r\n" + // + " }\r\n" + // + "}", 1, 1, 90, 20); + window.addButton("&Exec", 18, 22, new TAction() { + public void DO() { + SendEvent event = new SendEvent("EXEC"); + event.put("body", edit.getText()); + handleSubmit(event); + } + }); + + return window; + } + + private TWindow genReplaceClassWindow() { + TWindow window = new TWindow(this, "ReplaceClass", 0, 0, 100, 40, TWindow.HIDEONCLOSE); + window.addLabel("file path", 1, 2); + TField file = window.addField(18, 2, 77, false, ""); + + window.addLabel("class name", 1, 4); + TField cls = window.addField(18, 4, 77, false, ""); + + window.addButton("&Submit", 18, 6, new TAction() { + public void DO() { + showLog("replace class not supported"); + } + }); + return window; + } + + private void handleSubmit(SendEvent event) { + Map res = new HashMap<>(); + res.put("type", event.type); + res.put("id", event.id); + res.put("timestamp", event.timestamp); + res.putAll(event.details); + + ws.send(res); + } + + @Override + protected boolean onMenu(TMenuEvent menu) { + if (menu.getId() == 9999) { + this.exit(); + new Thread(() -> { + try { + Thread.sleep(1000L); + } catch (InterruptedException e) { + } + ws.close(); + System.out.println("good bye"); + System.exit(0); + }).start(); + return true; + } + if (menu.getId() >= 1005) { + id2Win.get(menu.getId()).show(); + return true; + } + + id2Win.forEach((k, v) -> { + if (k < 1005) { + if (k == menu.getId()) { + v.show(); + } else { + v.hide(); + } + } + }); + return true; + } + + public void showLog(String log) { + String now = dtf.format(LocalDateTime.now()); + log = now + " " + log; + logQueue.addFirst(log); + if (logQueue.size() > 100) { + logQueue.removeLast(); + } + String content = ""; + for (String l : logQueue) { + content = content + l + "\r\n"; + } + logger.setText(content); + } +} + +class SendEvent { + String type; + String id = UUID.randomUUID().toString().substring(0, 4); + long timestamp = System.currentTimeMillis(); + Map details = new HashMap<>(); + + public SendEvent(String type) { + this.type = type; + } + + public void put(String key, String value) { + details.put(key, value); + } +} + +interface Ws { + void send(Map map); + void close(); +} \ No newline at end of file diff --git a/lib/tools.jar b/lib/tools.jar new file mode 100644 index 0000000..65d5d7b Binary files /dev/null and b/lib/tools.jar differ diff --git a/pom.xml b/pom.xml index 48a0dd9..c9fa6ad 100644 --- a/pom.xml +++ b/pom.xml @@ -11,13 +11,12 @@ 1.8 - 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 @@ +