package services import ( "context" "errors" "fmt" "github.com/redis/go-redis/v9" "github.com/wailsapp/wails/v2/pkg/runtime" "strings" "sync" "tinyrdm/backend/types" sliceutil "tinyrdm/backend/utils/slice" strutil "tinyrdm/backend/utils/string" ) type cliService struct { ctx context.Context ctxCancel context.CancelFunc mutex sync.Mutex clients map[string]redis.UniversalClient selectedDB map[string]int } type cliOutput struct { Content []string `json:"content"` // output content Prompt string `json:"prompt,omitempty"` // new line prompt, empty if not ready to input } var cli *cliService var onceCli sync.Once func Cli() *cliService { if cli == nil { onceCli.Do(func() { cli = &cliService{ clients: map[string]redis.UniversalClient{}, selectedDB: map[string]int{}, } }) } return cli } func (c *cliService) runCommand(server, data string) { if cmds := strutil.SplitCmd(data); len(cmds) > 0 && len(cmds[0]) > 0 { if client, err := c.getRedisClient(server); err == nil { args := sliceutil.Map(cmds, func(i int) any { return cmds[i] }) if result, err := client.Do(c.ctx, args...).Result(); err == nil || errors.Is(err, redis.Nil) { if strings.ToLower(cmds[0]) == "select" { // switch database if db, ok := strutil.AnyToInt(cmds[1]); ok { c.selectedDB[server] = db } } c.echo(server, strutil.AnyToString(result, "", 0), true) } else { c.echoError(server, err.Error()) } return } } c.echoReady(server) } func (c *cliService) echo(server, data string, newLineReady bool) { output := cliOutput{ Content: strings.Split(data, "\n"), } if newLineReady { output.Prompt = fmt.Sprintf("%s:db%d> ", server, c.selectedDB[server]) } runtime.EventsEmit(c.ctx, "cmd:output:"+server, output) } func (c *cliService) echoReady(server string) { c.echo(server, "", true) } func (c *cliService) echoError(server, data string) { c.echo(server, "\x1b[31m"+data+"\x1b[0m", true) } func (c *cliService) getRedisClient(server string) (redis.UniversalClient, error) { c.mutex.Lock() defer c.mutex.Unlock() client, ok := c.clients[server] if !ok { var err error conf := Connection().getConnection(server) if conf == nil { return nil, fmt.Errorf("no connection profile named: %s", server) } if client, err = Connection().createRedisClient(conf.ConnectionConfig); err != nil { return nil, err } c.clients[server] = client } return client, nil } func (c *cliService) Start(ctx context.Context) { c.ctx, c.ctxCancel = context.WithCancel(ctx) } // StartCli start a cli session func (c *cliService) StartCli(server string, db int) (resp types.JSResp) { client, err := c.getRedisClient(server) if err != nil { resp.Msg = err.Error() return } client.Do(c.ctx, "select", db) c.selectedDB[server] = db // monitor input runtime.EventsOn(c.ctx, "cmd:input:"+server, func(data ...interface{}) { if len(data) > 0 { if str, ok := data[0].(string); ok { c.runCommand(server, str) return } } c.echoReady(server) }) // echo prefix c.echoReady(server) resp.Success = true return } // CloseCli close cli session func (c *cliService) CloseCli(server string) (resp types.JSResp) { c.mutex.Lock() defer c.mutex.Unlock() if client, ok := c.clients[server]; ok { client.Close() delete(c.clients, server) delete(c.selectedDB, server) } runtime.EventsOff(c.ctx, "cmd:input:"+server) resp.Success = true return } // CloseAll close all cli sessions func (c *cliService) CloseAll() { if c.ctxCancel != nil { c.ctxCancel() } for server := range c.clients { c.CloseCli(server) } }