├── .gitignore ├── README.md ├── go.mod ├── go.sum ├── hotfix ├── func_patch.go └── hotfix.go ├── symbols ├── github_com-cherry-game-cherry-hotfix-test-model.go └── symbols.go └── test ├── _patch_files └── foo.go.patch ├── foo_test.go └── model ├── m1 └── m1.go └── model.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | *.exe -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hotfix 2 | 基于yaegi + gomonkey技术,在运行时支持热刷go脚本,可动态替换函数、属性。 3 | 4 | ## 原理 5 | - 使用[yaegi](https://github.com/traefik/yaegi)动态执行`go`脚本。 6 | - 使用[gomonkey](https://github.com/agiledragon/gomonkey)进行函数打桩,完成函数替换。 7 | - [monkey原理](https://bou.ke/blog/monkey-patching-in-go/)技术实现细节。 8 | 9 | ## 支持平台 10 | - 架构 11 | - amd64 12 | - arm64 13 | - 386 14 | - loong64 15 | - 操作系统 16 | - Linux 17 | - MAC OS X 18 | - Windows 19 | 20 | ## 测试 21 | - 找到`test/foo_test.go`文件,运行`TestFixFooHelloFunc()`。 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cherry-game/cherry-hotfix 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/agiledragon/gomonkey/v2 v2.10.1 7 | github.com/traefik/yaegi v0.15.1 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/agiledragon/gomonkey/v2 v2.10.1 h1:FPJJNykD1957cZlGhr9X0zjr291/lbazoZ/dmc4mS4c= 2 | github.com/agiledragon/gomonkey/v2 v2.10.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= 3 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 4 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 5 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 6 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 7 | github.com/traefik/yaegi v0.15.1 h1:YA5SbaL6HZA0Exh9T/oArRHqGN2HQ+zgmCY7dkoTXu4= 8 | github.com/traefik/yaegi v0.15.1/go.mod h1:AVRxhaI2G+nUsaM1zyktzwXn69G3t/AuTDrCiTds9p0= 9 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 10 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 11 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 12 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 13 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 14 | -------------------------------------------------------------------------------- /hotfix/func_patch.go: -------------------------------------------------------------------------------- 1 | package hotfix 2 | 3 | import "reflect" 4 | 5 | type FuncPatch struct { 6 | StructType reflect.Type 7 | FuncName string 8 | FuncValue reflect.Value 9 | } 10 | -------------------------------------------------------------------------------- /hotfix/hotfix.go: -------------------------------------------------------------------------------- 1 | package hotfix 2 | 3 | import ( 4 | "errors" 5 | "github.com/agiledragon/gomonkey/v2" 6 | "github.com/traefik/yaegi/interp" 7 | "reflect" 8 | ) 9 | 10 | var ( 11 | convertFuncPatchErr = errors.New("convert FuncPatch error") 12 | retrieveMethodNameErr = errors.New("retrieve method by name failed") 13 | ) 14 | 15 | func ApplyFunc(filePath string, evalText string, symbols interp.Exports) (*gomonkey.Patches, error) { 16 | fp, err := loadFuncPatch(filePath, evalText, symbols) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | patches, err := monkeyFunc( 22 | fp.StructType, 23 | fp.FuncName, 24 | fp.FuncValue, 25 | ) 26 | 27 | return patches, err 28 | } 29 | 30 | func loadFuncPatch(filePath string, evalText string, symbols interp.Exports) (*FuncPatch, error) { 31 | // 构建解析器 32 | interpreter := interp.New(interp.Options{}) 33 | interpreter.Use(symbols) 34 | 35 | _, err := interpreter.EvalPath(filePath) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | // 获取替换函数 41 | res, err := interpreter.Eval(evalText) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | funcPatch, ok := res.Interface().(*FuncPatch) 47 | if !ok { 48 | return nil, convertFuncPatchErr 49 | } 50 | 51 | return funcPatch, nil 52 | } 53 | 54 | func monkeyFunc(source reflect.Type, methodName string, dest reflect.Value) (*gomonkey.Patches, error) { 55 | m, ok := source.MethodByName(methodName) 56 | if !ok { 57 | return nil, retrieveMethodNameErr 58 | } 59 | 60 | patches := gomonkey.NewPatches() 61 | return patches.ApplyCore(m.Func, dest), nil 62 | } 63 | -------------------------------------------------------------------------------- /symbols/github_com-cherry-game-cherry-hotfix-test-model.go: -------------------------------------------------------------------------------- 1 | // Code generated by 'yaegi extract github.com/cherry-game/cherry-hotfix/test/model'. DO NOT EDIT. 2 | 3 | package symbols 4 | 5 | import ( 6 | "github.com/cherry-game/cherry-hotfix/test/model" 7 | "reflect" 8 | ) 9 | 10 | func init() { 11 | Symbols["github.com/cherry-game/cherry-hotfix/test/model/model"] = map[string]reflect.Value{ 12 | // type definitions 13 | "Foo": reflect.ValueOf((*model.Foo)(nil)), 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /symbols/symbols.go: -------------------------------------------------------------------------------- 1 | package symbols 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/cherry-game/cherry-hotfix/hotfix" 7 | "github.com/traefik/yaegi/stdlib" 8 | ) 9 | 10 | var Symbols = map[string]map[string]reflect.Value{} 11 | 12 | func init() { 13 | for k, v := range stdlib.Symbols { 14 | Symbols[k] = v 15 | } 16 | 17 | Symbols["github.com/cherry-game/cherry-hotfix/hotfix/hotfix"] = map[string]reflect.Value{ 18 | // type definitions 19 | "FuncPatch": reflect.ValueOf((*hotfix.FuncPatch)(nil)), 20 | } 21 | } 22 | 23 | // 点击生成符号表 24 | //go:generate yaegi extract github.com/cherry-game/cherry-hotfix/test/model model 25 | -------------------------------------------------------------------------------- /test/_patch_files/foo.go.patch: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/cherry-game/cherry-hotfix/hotfix" 8 | "github.com/cherry-game/cherry-hotfix/test/model" 9 | ) 10 | 11 | func GetPatch() *hotfix.FuncPatch { 12 | fmt.Println("[Patch] invoke GetPatch()") 13 | 14 | fn := func(foo *model.Foo) string { 15 | foo.M1Int.Int = 1 16 | return "Hello() func is fixed" 17 | } 18 | 19 | return &hotfix.FuncPatch{ 20 | StructType: reflect.TypeOf(&model.Foo{}), 21 | FuncName: "Hello", 22 | FuncValue: reflect.ValueOf(fn), 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/foo_test.go: -------------------------------------------------------------------------------- 1 | package hotfix_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/cherry-game/cherry-hotfix/hotfix" 9 | "github.com/cherry-game/cherry-hotfix/test/model" 10 | 11 | "github.com/cherry-game/cherry-hotfix/symbols" 12 | ) 13 | 14 | func TestFixFooHelloFunc(t *testing.T) { 15 | foo1 := &model.Foo{ 16 | String: "foo1", 17 | } 18 | 19 | // 初始执行Hello(),并打印结果 20 | fmt.Printf("[Init] foo1:{%p}, Hello():{%v}\n", foo1, foo1.Hello()) 21 | 22 | // 模拟Hello()被调用 23 | for i := 0; i < 1000; i++ { 24 | go func(foo *model.Foo) { 25 | for { 26 | foo.Hello() 27 | time.Sleep(1 * time.Millisecond) 28 | } 29 | }(foo1) 30 | } 31 | 32 | var ( 33 | filePath = "./_patch_files/foo.go.patch" // 补丁脚本的路径 34 | evalText = "foo.GetPatch()" // 补丁脚本内执行的函数名 35 | ) 36 | 37 | // 加载补丁函数foo.GetPatch() 38 | patches, err := hotfix.ApplyFunc(filePath, evalText, symbols.Symbols) 39 | if err != nil { 40 | fmt.Println(err) 41 | return 42 | } 43 | 44 | // 打印已被替换的foo1.Hello() 45 | fmt.Printf("[Patch] foo1:{%p}, Hello():{%v}\n", foo1, foo1.Hello()) 46 | 47 | // 执行重置 48 | patches.Reset() 49 | 50 | // 打印函数 51 | fmt.Printf("[Reset] foo1:{%p}, Hello():{%v}\n", foo1, foo1.Hello()) 52 | } 53 | -------------------------------------------------------------------------------- /test/model/m1/m1.go: -------------------------------------------------------------------------------- 1 | package m1 2 | 3 | type M1 struct { 4 | Int int32 5 | } 6 | -------------------------------------------------------------------------------- /test/model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/cherry-game/cherry-hotfix/test/model/m1" 5 | ) 6 | 7 | type Foo struct { 8 | String string 9 | M1Int m1.M1 10 | } 11 | 12 | func (p *Foo) Hello() string { 13 | return p.String 14 | } 15 | --------------------------------------------------------------------------------