diff --git a/backend/services/monitor_service.go b/backend/services/monitor_service.go new file mode 100644 index 0000000..b14de65 --- /dev/null +++ b/backend/services/monitor_service.go @@ -0,0 +1,174 @@ +package services + +import ( + "bufio" + "context" + "errors" + "fmt" + "github.com/redis/go-redis/v9" + "github.com/wailsapp/wails/v2/pkg/runtime" + "os" + "strconv" + "sync" + "time" + "tinyrdm/backend/types" +) + +type monitorItem struct { + client *redis.Client + cmd *redis.MonitorCmd + ch chan string + closeCh chan struct{} + eventName string +} + +type monitorService struct { + ctx context.Context + ctxCancel context.CancelFunc + mutex sync.Mutex + items map[string]*monitorItem +} + +var monitor *monitorService +var onceMonitor sync.Once + +func Monitor() *monitorService { + if monitor == nil { + onceMonitor.Do(func() { + monitor = &monitorService{ + items: map[string]*monitorItem{}, + } + }) + } + return monitor +} + +func (c *monitorService) getItem(server string) (*monitorItem, error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + item, ok := c.items[server] + if !ok { + var err error + conf := Connection().getConnection(server) + if conf == nil { + return nil, fmt.Errorf("no connection profile named: %s", server) + } + var uniClient redis.UniversalClient + if uniClient, err = Connection().createRedisClient(conf.ConnectionConfig); err != nil { + return nil, err + } + var client *redis.Client + if client, ok = uniClient.(*redis.Client); !ok { + return nil, errors.New("create redis client fail") + } + item = &monitorItem{ + client: client, + } + c.items[server] = item + } + return item, nil +} + +func (c *monitorService) Start(ctx context.Context) { + c.ctx, c.ctxCancel = context.WithCancel(ctx) +} + +// StartMonitor start a monitor by server name +func (c *monitorService) StartMonitor(server string) (resp types.JSResp) { + item, err := c.getItem(server) + if err != nil { + resp.Msg = err.Error() + return + } + + item.ch = make(chan string) + item.closeCh = make(chan struct{}) + item.eventName = "monitor:" + strconv.Itoa(int(time.Now().Unix())) + item.cmd = item.client.Monitor(c.ctx, item.ch) + item.cmd.Start() + + go c.processMonitor(item.ch, item.closeCh, item.eventName) + resp.Success = true + resp.Data = struct { + EventName string `json:"eventName"` + }{ + EventName: item.eventName, + } + return +} + +func (c *monitorService) processMonitor(ch <-chan string, closeCh <-chan struct{}, eventName string) { + for { + select { + case data := <-ch: + if data != "OK" { + runtime.EventsEmit(c.ctx, eventName, data) + } + + case <-closeCh: + // monitor stopped + return + } + } +} + +// StopMonitor stop monitor by server name +func (c *monitorService) StopMonitor(server string) (resp types.JSResp) { + c.mutex.Lock() + defer c.mutex.Unlock() + + item, ok := c.items[server] + if !ok || item.cmd == nil { + resp.Success = true + return + } + + item.cmd.Stop() + //close(item.ch) + close(item.closeCh) + delete(c.items, server) + resp.Success = true + return +} + +// StopAll stop all monitor +func (c *monitorService) StopAll() { + if c.ctxCancel != nil { + c.ctxCancel() + } + + for server := range c.items { + c.StopMonitor(server) + } +} + +func (c *monitorService) ExportLog(logs []string) (resp types.JSResp) { + filepath, err := runtime.SaveFileDialog(c.ctx, runtime.SaveDialogOptions{ + ShowHiddenFiles: false, + DefaultFilename: fmt.Sprintf("monitor_log_%s.txt", time.Now().Format("20060102150405")), + Filters: []runtime.FileFilter{ + {Pattern: "*.txt"}, + }, + }) + if err != nil { + resp.Msg = err.Error() + return + } + + file, err := os.Create(filepath) + if err != nil { + resp.Msg = err.Error() + return + } + defer file.Close() + + writer := bufio.NewWriter(file) + for _, line := range logs { + _, _ = writer.WriteString(line + "\n") + } + writer.Flush() + + resp.Success = true + return +} diff --git a/frontend/src/components/content/ContentLogPane.vue b/frontend/src/components/content/ContentLogPane.vue index f3c532c..6c047a6 100644 --- a/frontend/src/components/content/ContentLogPane.vue +++ b/frontend/src/components/content/ContentLogPane.vue @@ -133,12 +133,8 @@ defineExpose({ diff --git a/frontend/src/components/content/ContentPane.vue b/frontend/src/components/content/ContentPane.vue index 3fff5e3..ff73259 100644 --- a/frontend/src/components/content/ContentPane.vue +++ b/frontend/src/components/content/ContentPane.vue @@ -15,6 +15,7 @@ import Monitor from '@/components/icons/Monitor.vue' import Pub from '@/components/icons/Pub.vue' import ContentSlog from '@/components/content_value/ContentSlog.vue' import { decodeTypes, formatTypes } from '@/consts/value_view_type.js' +import ContentMonitor from '@/components/content_value/ContentMonitor.vue' const themeVars = useThemeVars() @@ -173,7 +174,7 @@ watch( - + + diff --git a/frontend/src/components/content_value/ContentMonitor.vue b/frontend/src/components/content_value/ContentMonitor.vue new file mode 100644 index 0000000..fc6dc6d --- /dev/null +++ b/frontend/src/components/content_value/ContentMonitor.vue @@ -0,0 +1,170 @@ + + + + + diff --git a/frontend/src/components/content_value/ContentSlog.vue b/frontend/src/components/content_value/ContentSlog.vue index 34b80f8..d782cc4 100644 --- a/frontend/src/components/content_value/ContentSlog.vue +++ b/frontend/src/components/content_value/ContentSlog.vue @@ -150,12 +150,7 @@ const onListLimitChanged = (limit) => { diff --git a/frontend/src/components/icons/Play.vue b/frontend/src/components/icons/Play.vue new file mode 100644 index 0000000..3a820b5 --- /dev/null +++ b/frontend/src/components/icons/Play.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/frontend/src/langs/en-us.json b/frontend/src/langs/en-us.json index 31ee190..5ea949c 100644 --- a/frontend/src/langs/en-us.json +++ b/frontend/src/langs/en-us.json @@ -353,5 +353,16 @@ "cost_time": "Cost", "refresh": "Refresh Now", "auto_refresh": "Auto Refresh" + }, + "monitor": { + "title": "Monitor Commands", + "actions": "Actions", + "start": "Start", + "stop": "Stop", + "search": "Search", + "copy_log": "Copy Log", + "save_log": "Save Log", + "clean_log": "Clean Log", + "always_show_last": "Always To Last Line" } } diff --git a/frontend/src/langs/zh-cn.json b/frontend/src/langs/zh-cn.json index 5958f61..7a2189b 100644 --- a/frontend/src/langs/zh-cn.json +++ b/frontend/src/langs/zh-cn.json @@ -353,5 +353,16 @@ "cost_time": "耗时", "refresh": "立即刷新", "auto_refresh": "自动刷新" + }, + "monitor": { + "title": "监控命令", + "actions": "操作", + "start": "开启监控", + "stop": "停止监控", + "search": "搜索", + "copy_log": "复制日志", + "save_log": "保存日志", + "clean_log": "清空日志", + "always_show_last": "总是显示最新" } } diff --git a/frontend/src/styles/content.scss b/frontend/src/styles/content.scss index c524eb1..fca7bc9 100644 --- a/frontend/src/styles/content.scss +++ b/frontend/src/styles/content.scss @@ -9,6 +9,10 @@ justify-content: center; } +.content-log { + padding: 20px; +} + .content-value { user-select: text; } diff --git a/main.go b/main.go index aece126..9640278 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,7 @@ func main() { connSvc := services.Connection() browserSvc := services.Browser() cliSvc := services.Cli() + monitorSvc := services.Monitor() prefSvc := services.Preferences() prefSvc.SetAppVersion(version) windowWidth, windowHeight, maximised := prefSvc.GetWindowSize() @@ -68,6 +69,7 @@ func main() { connSvc.Start(ctx) browserSvc.Start(ctx) cliSvc.Start(ctx) + monitorSvc.Start(ctx) services.GA().SetSecretKey(gaMeasurementID, gaSecretKey) services.GA().Startup(version) @@ -85,12 +87,14 @@ func main() { OnShutdown: func(ctx context.Context) { browserSvc.Stop() cliSvc.CloseAll() + monitorSvc.StopAll() }, Bind: []interface{}{ sysSvc, connSvc, browserSvc, cliSvc, + monitorSvc, prefSvc, }, Mac: &mac.Options{