From 7517e4652a04151ffab7c293010eb392790c172f Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 20 Aug 2022 21:25:32 +0800 Subject: [PATCH] [change] Refactoring Watch --- cmd/root.go | 1 + cmd/start.go | 20 +- go.mod | 4 +- go.sum | 4 + service/service.go | 16 ++ service/watch.go | 600 ++++++++++++++---------------------------- service/watch_test.go | 153 +++++++++++ 7 files changed, 391 insertions(+), 407 deletions(-) create mode 100644 service/watch_test.go diff --git a/cmd/root.go b/cmd/root.go index eec2ca8c..eb095106 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -45,6 +45,7 @@ var langs = map[string]string{ "NEXT:": "下一步:", "Listening": " 监听", "✨LISTENING✨": "✨服务正在运行✨", + "✨STOPPED✨": "✨服务已停止✨", "SessionPort": "会话服务端口", "Force migrate": "强制更新数据表结构", "Migrate is not allowed on production mode.": "Migrate 不能再生产环境下使用", diff --git a/cmd/start.go b/cmd/start.go index 516d6362..7c00bf7b 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -3,8 +3,10 @@ package cmd import ( "fmt" "os" + "os/signal" "path/filepath" "strings" + "syscall" "github.com/fatih/color" "github.com/spf13/cobra" @@ -24,6 +26,11 @@ var startCmd = &cobra.Command{ Short: L("Start Engine"), Long: L("Start Engine"), Run: func(cmd *cobra.Command, args []string) { + + // recive interrupt signal + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT) + // defer service.Stop(func() { fmt.Println(L("Service stopped")) }) Boot() @@ -94,9 +101,18 @@ var startCmd = &cobra.Command{ if mode == "development" { service.Watch(config.Conf) } - + go service.Start() fmt.Println(color.GreenString(L("✨LISTENING✨"))) - service.Start() + + for { + select { + case <-interrupt: + service.Stop(func() { + fmt.Println(color.GreenString(L("✨STOPPED✨"))) + }) + return + } + } }, } diff --git a/go.mod b/go.mod index ac673cbd..b22f638f 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/caarlos0/env/v6 v6.9.1 github.com/elazarl/go-bindata-assetfs v1.0.1 github.com/fatih/color v1.13.0 - github.com/fsnotify/fsnotify v1.5.1 + github.com/fsnotify/fsnotify v1.5.4 github.com/gin-gonic/gin v1.7.7 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/joho/godotenv v1.3.0 @@ -63,6 +63,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/richardlehane/mscfb v1.0.3 // indirect github.com/richardlehane/msoleps v1.0.1 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/satori/go.uuid v1.2.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -76,7 +77,6 @@ require ( github.com/tidwall/rtred v0.1.2 // indirect github.com/tidwall/tinyqueue v0.1.1 // indirect github.com/ugorji/go/codec v1.1.7 // indirect - github.com/robfig/cron/v3 v3.0.1 // indirect github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3 // indirect golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect golang.org/x/mod v0.4.2 // indirect diff --git a/go.sum b/go.sum index 5f1c8c17..e0131174 100644 --- a/go.sum +++ b/go.sum @@ -97,6 +97,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= @@ -581,6 +583,7 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -773,6 +776,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/service/service.go b/service/service.go index 5da3ca6d..cec86fbd 100644 --- a/service/service.go +++ b/service/service.go @@ -1,7 +1,10 @@ package service import ( + "context" + "github.com/yaoapp/gou" + "github.com/yaoapp/kun/log" "github.com/yaoapp/yao/config" "github.com/yaoapp/yao/share" ) @@ -68,3 +71,16 @@ func Stop(onComplete func()) { onComplete() } } + +// StopWithContext stop with timeout +func StopWithContext(ctx context.Context, onComplete func()) { + shutdown <- true + select { + case <-ctx.Done(): + log.Error("[STOP] canceled (%v)", ctx.Err()) + onComplete() + case <-shutdownComplete: + share.SessionStop() + onComplete() + } +} diff --git a/service/watch.go b/service/watch.go index 92247d3f..4f468831 100644 --- a/service/watch.go +++ b/service/watch.go @@ -1,459 +1,253 @@ package service import ( + "context" "fmt" + "io/fs" + "io/ioutil" "os" + "os/signal" "path/filepath" "strings" + "syscall" + "time" "github.com/fatih/color" + "github.com/fsnotify/fsnotify" "github.com/yaoapp/gou" - "github.com/yaoapp/yao/chart" + "github.com/yaoapp/kun/log" "github.com/yaoapp/yao/config" "github.com/yaoapp/yao/engine" - "github.com/yaoapp/yao/page" "github.com/yaoapp/yao/share" - "github.com/yaoapp/yao/table" - "github.com/yaoapp/yao/workflow" ) -// Watch 监听应用目录文件变更 -func Watch(cfg config.Config) { - if os.Getenv("YAO_DEV") != "" { - WatchEngine(filepath.Join(os.Getenv("YAO_DEV"), "/yao")) - } - WatchModel(filepath.Join(cfg.Root, "models"), "") - WatchAPI(filepath.Join(cfg.Root, "apis"), "") - WatchFlow(filepath.Join(cfg.Root, "flows"), "") - WatchPlugin(filepath.Join(cfg.Root, "plugins")) - WatchTable(filepath.Join(cfg.Root, "tables"), "") - WatchChart(filepath.Join(cfg.Root, "charts"), "") - WatchPage(filepath.Join(cfg.Root, "pages"), "") - WatchWorkFlow(filepath.Join(cfg.Root, "workflows"), "") - - // 看板大屏 - WatchPage(filepath.Join(cfg.Root, "kanban"), "") - WatchPage(filepath.Join(cfg.Root, "screen"), "") - - // 监听脚本 & libs更新 - WatchGlobal(filepath.Join(cfg.Root, "libs")) - WatchGlobal(filepath.Join(cfg.Root, "scripts")) +var watchShutdown = make(chan bool, 1) // shutdown signal +var watchReady = make(chan bool, 1) // ready signal +var excludes = map[string]bool{"ui": true, "db": true, "data": true} +var hanlders = map[string]func(root string, file string, event string, cfg config.Config){ + "models": watchModel, } -// WatchEngine 监听监听引擎内建数据变更 -func WatchEngine(root string) { - root = share.DirAbs(root) - WatchModel(filepath.Join(root, "models"), "xiang.") - WatchAPI(filepath.Join(root, "apis"), "xiang.") - WatchFlow(filepath.Join(root, "flows"), "xiang.") - WatchTable(filepath.Join(root, "tables"), "xiang.") +// Watch the application code change for hot update +func Watch(cfg config.Config) (err error) { + go func() { err = watchStart(cfg) }() + select { + case <-watchReady: + return nil + } } -// WatchGlobal 监听通用程序更新 -func WatchGlobal(root string) { - if share.DirNotExists(root) { - return +// StopWatch stop watching the code change +func StopWatch() { + watchShutdown <- true + time.Sleep(200 * time.Millisecond) +} + +func watchStart(cfg config.Config) error { + + root := cfg.Root + + // recive interrupt signal + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT) + + shutdown := make(chan bool, 1) + + watcher, err := fsnotify.NewWatcher() + if err != nil { + return err } - root = share.DirAbs(root) - go share.Watch(root, func(op string, filename string) { - if !strings.HasSuffix(filename, ".json") && !strings.HasSuffix(filename, ".js") { - return + defer watcher.Close() + + root, err = filepath.Abs(root) + if err != nil { + return err + } + + dirs, err := ioutil.ReadDir(root) + if err != nil { + return err + } + + // Add path + for _, dir := range dirs { + if !dir.IsDir() { + continue } - // 重启服务器 - if op == "write" || op == "create" || op == "remove" || op == "rename" { - err := engine.Load(config.Conf) + name := dir.Name() + if _, has := hanlders[name]; !has { + continue + } + + filename := filepath.Join(root, name) + err := watcher.Add(filename) + if err != nil { + return err + } + + fmt.Println(color.GreenString("[Watch] Watching %s", name)) + log.Info("[Watch] Watching: %s", filename) + + // sub dir + err = filepath.WalkDir(filename, func(path string, d fs.DirEntry, err error) error { + if !d.IsDir() { + return nil + } + + log.Info("[Watch] Watching: %s", path) + err = watcher.Add(path) if err != nil { - fmt.Println(color.RedString("Fatal: %s", err.Error())) - return + return err } - StopWithouttSession(func() { - fmt.Println(color.GreenString("Service Restarted")) - go StartWithouttSession() - }) - } - }) -} + return nil + }) -// WatchModel 监听业务接口更新 -func WatchModel(root string, prefix string) { - if share.DirNotExists(root) { - return + if err != nil { + return err + } } - root = share.DirAbs(root) - go share.Watch(root, func(op string, filename string) { - if !strings.HasSuffix(filename, ".json") { - return - } - if op == "write" || op == "create" { - name := prefix + share.SpecName(root, filename) - content := share.ReadFile(filename) - _, err := gou.LoadModelReturn(string(content), name) // Reload - if err != nil { - fmt.Println(color.RedString("Fatal: %s", err.Error())) + // event hanlder + go func() { + for { + select { + case <-shutdown: + log.Info("[Watch] The event handler exit") return - } - fmt.Println(color.GreenString("Model %s Reloaded", name)) - } else if op == "remove" || op == "rename" { - name := prefix + share.SpecName(root, filename) - if _, has := gou.Models[name]; has { - delete(gou.Models, name) - fmt.Println(color.RedString("Model %s Removed", name)) - } - } - }) -} -// WatchAPI 监听业务接口更新 -func WatchAPI(root string, prefix string) { - if share.DirNotExists(root) { - return - } - root = share.DirAbs(root) - go share.Watch(root, func(op string, filename string) { + case event, ok := <-watcher.Events: + if !ok { + return + } + relpath := strings.TrimPrefix(event.Name, root) + if strings.HasPrefix(relpath, string(os.PathSeparator)) { + relpath = strings.TrimPrefix(relpath, string(os.PathSeparator)) + } - if !strings.HasSuffix(filename, ".json") { - return - } + pi := strings.Split(relpath, string(os.PathSeparator)) + widget := pi[0] - if op == "write" || op == "create" { - name := prefix + share.SpecName(root, filename) - content := share.ReadFile(filename) - _, err := gou.LoadAPIReturn(string(content), name) // Reload - if err != nil { - fmt.Println(color.RedString("Fatal: %s", err.Error())) - return - } - fmt.Println(color.GreenString("API %s Reloaded", name)) + watchHanler := watchReload + if hanlder, has := hanlders[widget]; has { + watchHanler = hanlder + } - } else if op == "remove" || op == "rename" { - name := prefix + share.SpecName(root, filename) - if _, has := gou.APIs[name]; has { - delete(gou.APIs, name) - fmt.Println(color.RedString("API %s Removed", name)) - } - } - - // 重启服务器 - if op == "write" || op == "create" || op == "remove" || op == "rename" { - StopWithouttSession(func() { - fmt.Println(color.GreenString("Service Restarted")) - go StartWithouttSession() - }) - } - }) -} - -// WatchFlow 监听业务逻辑变更 -func WatchFlow(root string, prefix string) { - if share.DirNotExists(root) { - return - } - root = share.DirAbs(root) - go share.Watch(root, func(op string, filename string) { - if !strings.HasSuffix(filename, ".json") && !strings.HasSuffix(filename, ".js") { - return - } - - if strings.HasSuffix(filename, ".js") { - name := prefix + share.SpecName(root, filename) - name = strings.ReplaceAll(name, ".", "/") - filename = filepath.Join(root, name+".flow.json") - } - - if op == "write" || op == "create" { - name := prefix + share.SpecName(root, filename) - content := share.ReadFile(filename) - flow, err := gou.LoadFlowReturn(string(content), name) // Reload - if err != nil { - fmt.Println(color.RedString("Fatal: %s", err.Error())) - return - } - - if flow != nil { // Reload Script - dir := filepath.Dir(filename) - share.Walk(dir, ".js", func(root, filename string) { - script := share.ScriptName(filename) - content := share.ReadFile(filename) - _, err := flow.LoadScriptReturn(string(content), script) - if err != nil { - fmt.Println(color.RedString("Fatal: %s", err.Error())) - return + if _, has := excludes[widget]; !has { + base := filepath.Base(event.Name) + isdir := true + if strings.HasSuffix(base, ".yao") || strings.HasSuffix(base, ".json") { + isdir = false } - }) - } - fmt.Println(color.GreenString("Flow %s Reloaded", name)) + events := strings.Split(event.Op.String(), "|") + for _, eventType := range events { - } else if op == "remove" || op == "rename" { - name := prefix + share.SpecName(root, filename) - if _, has := gou.Flows[name]; has { - delete(gou.Flows, name) - fmt.Println(color.RedString("Flow %s Removed", name)) - } - } - }) -} + // ADD / REMOVE Watching dir + if isdir { + switch eventType { + case "CREATE": + log.Info("[Watch] Watching: %s", event.Name) + watcher.Add(event.Name) + break + case "REMOVE": + log.Info("[Watch] Unwatching: %s", event.Name) + watcher.Remove(event.Name) + break + } + continue + } -// WatchPlugin 监听业务插件变更 -func WatchPlugin(root string) { - if share.DirNotExists(root) { - return - } - root = share.DirAbs(root) - go share.Watch(root, func(op string, filename string) { - - if !strings.HasSuffix(filename, ".so") { - return - } - - if op == "write" || op == "create" { - name := share.SpecName(root, filename) - _, err := gou.LoadPluginReturn(filename, name) // Reload - if err != nil { - fmt.Println(color.RedString("Fatal: %s", err.Error())) - return - } - fmt.Println(color.GreenString("Plugin %s Reloaded", name)) - - } else if op == "remove" || op == "rename" { - name := share.SpecName(root, filename) - if _, has := gou.Plugins[name]; has { - delete(gou.Plugins, name) - fmt.Println(color.RedString("Plugin %s Removed", name)) - } - } - }) -} - -// WatchTable 监听数据表格更新 -func WatchTable(root string, prefix string) { - if share.DirNotExists(root) { - return - } - root = share.DirAbs(root) - go share.Watch(root, func(op string, filename string) { - - if !strings.HasSuffix(filename, ".json") { - return - } - - if op == "write" || op == "create" { - name := prefix + share.SpecName(root, filename) - content := share.ReadFile(filename) - _, err := table.LoadTable(string(content), name) // Reload Table - if err != nil { - fmt.Println(color.RedString("Fatal: %s", err.Error())) - return - } - - api, has := gou.APIs["xiang.table"] - if has { - _, err := gou.LoadAPIReturn(api.Source, api.Name) - if err != nil { - fmt.Println(color.RedString("Fatal: %s", err.Error())) - return - } - } - fmt.Println(color.GreenString("Table %s Reloaded", name)) - - } else if op == "remove" || op == "rename" { - name := prefix + share.SpecName(root, filename) - if _, has := table.Tables[name]; has { - delete(table.Tables, name) - fmt.Println(color.RedString("Table %s Removed", name)) - } - } - - // 重启服务器 - if op == "write" || op == "create" || op == "remove" || op == "rename" { - StopWithouttSession(func() { - fmt.Println(color.GreenString("Service Restarted")) - go StartWithouttSession() - }) - } - }) -} - -// WatchChart 监听分析图表更新 -func WatchChart(root string, prefix string) { - if share.DirNotExists(root) { - return - } - root = share.DirAbs(root) - go share.Watch(root, func(op string, filename string) { - if !strings.HasSuffix(filename, ".json") && !strings.HasSuffix(filename, ".js") { - return - } - - if strings.HasSuffix(filename, ".js") { - name := prefix + share.SpecName(root, filename) - name = strings.ReplaceAll(name, ".", "/") - filename = filepath.Join(root, name+".chart.json") - } - - if op == "write" || op == "create" { - name := prefix + share.SpecName(root, filename) - content := share.ReadFile(filename) - chart, err := chart.LoadChart(content, name) // Relaod - if err != nil { - fmt.Println(color.RedString("Fatal: %s", err.Error())) - return - } - - if chart != nil { // Reload Script - dir := filepath.Dir(filename) - share.Walk(dir, ".js", func(root, filename string) { - script := share.ScriptName(filename) - content := share.ReadFile(filename) - _, err := chart.LoadScriptReturn(string(content), script) - if err != nil { - fmt.Println(color.RedString("Fatal: %s", err.Error())) - return + log.Info("[Watch] %s %s", eventType, event.Name) + watchHanler(root, event.Name, eventType, cfg) } - }) - } + } + break - api, has := gou.APIs["xiang.chart"] - if has { - _, err := gou.LoadAPIReturn(api.Source, api.Name) - if err != nil { - fmt.Println(color.RedString("Fatal: %s", err.Error())) + case err, ok := <-watcher.Errors: + if !ok { return } - } - fmt.Println(color.GreenString("Chart %s Reloaded", name)) - - } else if op == "remove" || op == "rename" { - name := prefix + share.SpecName(root, filename) - if _, has := chart.Charts[name]; has { - delete(chart.Charts, name) - fmt.Println(color.RedString("Chart %s Removed", name)) + fmt.Println(color.RedString("[Watch] %s", err.Error())) + log.Error("[Watch] %s", err.Error()) + break } } + }() - // 重启服务器 - if op == "write" || op == "create" || op == "remove" || op == "rename" { - StopWithouttSession(func() { - fmt.Println(color.GreenString("Service Restarted")) - go StartWithouttSession() - }) + fmt.Println(color.GreenString("[Watch] Started")) + watchReady <- true + + for { + select { + case <-watchShutdown: + shutdown <- true + log.Info("[Watch] Stopped") + fmt.Println(color.YellowString("[Watch] Stopped")) + return nil + + case <-interrupt: + shutdown <- true + log.Info("[Watch] Stopped") + fmt.Println(color.YellowString("[Watch] Stopped")) + return nil } - }) + } } -// WatchPage 监听页面更新 -func WatchPage(root string, prefix string) { - if share.DirNotExists(root) { - return - } - root = share.DirAbs(root) - go share.Watch(root, func(op string, filename string) { - if !strings.HasSuffix(filename, ".json") && !strings.HasSuffix(filename, ".js") { +func watchModel(root string, file string, event string, cfg config.Config) { + name := share.SpecName(root, file) + switch event { + case "CREATE": + content, err := ioutil.ReadFile(file) + if err != nil { + fmt.Println(color.RedString("[Watch] Model: %s %s", name, err.Error())) + return + } + mod, err := gou.LoadModelReturn(string(content), name) + if err != nil { + fmt.Println(color.RedString("[Watch] Model: %s %s", name, err.Error())) return } - if strings.HasSuffix(filename, ".js") { - name := prefix + share.SpecName(root, filename) - name = strings.ReplaceAll(name, ".", "/") - filename = filepath.Join(root, name+".page.json") - } + mod.Migrate(true) + fmt.Println(color.GreenString("[Watch] Model: %s Created", name)) + break - if op == "write" || op == "create" { - name := prefix + share.SpecName(root, filename) - content := share.ReadFile(filename) - page, err := page.LoadPage(content, name) // Relaod - if err != nil { - fmt.Println(color.RedString("Fatal: %s", err.Error())) - return - } - - if page != nil { // Reload Script - dir := filepath.Dir(filename) - share.Walk(dir, ".js", func(root, filename string) { - script := share.ScriptName(filename) - content := share.ReadFile(filename) - _, err := page.LoadScriptReturn(string(content), script) - if err != nil { - fmt.Println(color.RedString("Fatal: %s", err.Error())) - return - } - }) - } - - api, has := gou.APIs["xiang.page"] - if has { - _, err := gou.LoadAPIReturn(api.Source, api.Name) - if err != nil { - fmt.Println(color.RedString("Fatal: %s", err.Error())) - return - } - } - fmt.Println(color.GreenString("Page %s Reloaded", name)) - - } else if op == "remove" || op == "rename" { - name := prefix + share.SpecName(root, filename) - if _, has := page.Pages[name]; has { - delete(page.Pages, name) - fmt.Println(color.RedString("Page %s Removed", name)) - } - } - - // 重启服务器 - if op == "write" || op == "create" || op == "remove" || op == "rename" { - StopWithouttSession(func() { - fmt.Println(color.GreenString("Service Restarted")) - go StartWithouttSession() - }) - } - }) -} - -// WatchWorkFlow 监听工作流更新 -func WatchWorkFlow(root string, prefix string) { - if share.DirNotExists(root) { - return - } - root = share.DirAbs(root) - go share.Watch(root, func(op string, filename string) { - if !strings.HasSuffix(filename, ".json") { + case "WRITE": + content, err := ioutil.ReadFile(file) + if err != nil { + fmt.Println(color.RedString("[Watch] Model: %s %s", name, err.Error())) return } - - if op == "write" || op == "create" { - name := prefix + share.SpecName(root, filename) - content := share.ReadFile(filename) - _, err := workflow.LoadWorkFlow(content, name) // Relaod - if err != nil { - fmt.Println(color.RedString("Fatal: %s", err.Error())) - return - } - - api, has := gou.APIs["xiang.workflow."+name] - if has { - _, err := gou.LoadAPIReturn(api.Source, api.Name) - if err != nil { - fmt.Println(color.RedString("Fatal: %s", err.Error())) - return - } - } - fmt.Println(color.GreenString("Workflow %s Reloaded", name)) - - } else if op == "remove" || op == "rename" { - name := prefix + share.SpecName(root, filename) - if _, has := workflow.WorkFlows[name]; has { - delete(workflow.WorkFlows, name) - fmt.Println(color.RedString("Workflow %s Removed", name)) - } + mod, err := gou.LoadModelReturn(string(content), name) + if err != nil { + fmt.Println(color.RedString("[Watch] Model: %s %s", name, err.Error())) + return } + mod.Migrate(false) + fmt.Println(color.GreenString("[Watch] Model: %s Reloaded", name)) + break - // 重启服务器 - if op == "write" || op == "create" || op == "remove" || op == "rename" { - StopWithouttSession(func() { - fmt.Println(color.GreenString("Service Restarted")) - go StartWithouttSession() - }) - } + case "REMOVE", "RENAME": + delete(gou.Models, name) + fmt.Println(color.GreenString("[Watch] Model: %s Removed", name)) + break + } +} + +func watchReload(root string, file string, event string, cfg config.Config) { + err := engine.Load(config.Conf) // 加载脚本等 + if err != nil { + fmt.Println(color.RedString("[Watch] Reload: %s", err.Error())) + } + + // Restart Server + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + StopWithContext(ctx, func() { + go Start() + fmt.Println(color.GreenString("[Watch] Reload Completed")) }) } diff --git a/service/watch_test.go b/service/watch_test.go new file mode 100644 index 00000000..e1cd9b37 --- /dev/null +++ b/service/watch_test.go @@ -0,0 +1,153 @@ +package service + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" + "time" + + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/share" +) + +func TestWatch(t *testing.T) { + + share.DBConnect(config.Conf.DB) + err := Watch(config.Conf) + if err != nil { + t.Fatal(err) + } + defer StopWatch() + + createDir(t) + renameDir(t) + + createModel(t) + changeModel(t) + renameModel(t) + removeModel(t) + + createModel(t) + removeDir(t) +} + +func TestWatchReload(t *testing.T) { + go Start() + defer Stop(func() {}) + share.DBConnect(config.Conf.DB) + watchReload("", "", "", config.Conf) +} + +func createDir(t *testing.T) { + root := config.Conf.Root + file := filepath.Join(root, "models", "watch", "test") + fmt.Println("CREATE-DIR", file) + err := os.MkdirAll(file, os.ModePerm) + if err != nil { + t.Fatal(err) + } + time.Sleep(1 * time.Second) +} + +func renameDir(t *testing.T) { + root := config.Conf.Root + file := filepath.Join(root, "models", "watch", "test") + new := filepath.Join(root, "models", "watch", "test_new") + fmt.Println("RENAME-DIR", file) + err := os.Rename(file, new) + if err != nil { + t.Fatal(err) + } + time.Sleep(1 * time.Second) +} + +func removeDir(t *testing.T) { + root := config.Conf.Root + file := filepath.Join(root, "models", "watch") + fmt.Println("REMOVE-DIR", file) + err := os.RemoveAll(file) + if err != nil { + t.Fatal(err) + } + time.Sleep(1 * time.Second) +} + +func createModel(t *testing.T) { + dsl := ` + { + "name": "watch-test", + "table": { + "name": "watch_test", + "comment": "WatchTest", + "engine": "InnoDB" + }, + "columns": [ + { "name": "id", "type": "ID" }, + { "label": "Name", "name": "name", "type": "string", "index": true } + ], + "relations": {}, + "option": { "timestamps": true, "soft_deletes": true } + } + ` + root := config.Conf.Root + file := filepath.Join(root, "models", "watch", "test_new", "watch.mod.json") + fmt.Println("CREATE", file) + err := ioutil.WriteFile(file, []byte(dsl), 0644) + if err != nil { + t.Fatal(err) + } + time.Sleep(1 * time.Second) +} + +func changeModel(t *testing.T) { + dsl := ` + { + "name": "watch-test", + "table": { + "name": "watch_test", + "comment": "WatchTest", + "engine": "InnoDB" + }, + "columns": [ + { "name": "id", "type": "ID" }, + { "label": "Name", "name": "name", "type": "string", "index": true }, + { "label": "Data", "name": "data", "type": "json", "nullable": true } + ], + "relations": {}, + "option": { "timestamps": true, "soft_deletes": true } + } + ` + root := config.Conf.Root + file := filepath.Join(root, "models", "watch", "test_new", "watch.mod.json") + fmt.Println("CHANGE", file) + err := ioutil.WriteFile(file, []byte(dsl), 0644) + if err != nil { + t.Fatal(err) + } + time.Sleep(1 * time.Second) +} + +func renameModel(t *testing.T) { + root := config.Conf.Root + file := filepath.Join(root, "models", "watch", "test_new", "watch.mod.json") + new := filepath.Join(root, "models", "watch", "test_new", "watch_new.mod.json") + fmt.Println("RENAME", new) + err := os.Rename(file, new) + if err != nil { + t.Fatal(err) + } + time.Sleep(1 * time.Second) +} + +func removeModel(t *testing.T) { + root := config.Conf.Root + file := filepath.Join(root, "models", "watch", "test_new", "watch_new.mod.json") + fmt.Println("REMOVE", file) + err := os.Remove(file) + if err != nil { + t.Fatal(err) + } + time.Sleep(1 * time.Second) +}