From 4ce4e1af7101bf21832f9565a09f8e99804194a5 Mon Sep 17 00:00:00 2001 From: Josh Baker Date: Tue, 29 Mar 2016 12:29:15 -0700 Subject: [PATCH] optional output formats --- cmd/tile38-cli/main.go | 118 +++++++++++++++++++---- controller/controller.go | 25 +++-- controller/hooks.go | 186 +++++++++++++++++++++++++----------- controller/output.go | 40 ++++++++ controller/server/server.go | 6 ++ 5 files changed, 294 insertions(+), 81 deletions(-) create mode 100644 controller/output.go diff --git a/cmd/tile38-cli/main.go b/cmd/tile38-cli/main.go index ed162d95..35b18392 100644 --- a/cmd/tile38-cli/main.go +++ b/cmd/tile38-cli/main.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "encoding/json" "fmt" "io" @@ -13,6 +14,7 @@ import ( "strings" "github.com/peterh/liner" + "github.com/tidwall/resp" "github.com/tidwall/tile38/client" "github.com/tidwall/tile38/core" ) @@ -39,9 +41,11 @@ type connError struct { var ( hostname = "127.0.0.1" + output = "json" port = 9851 oneCommand string tokml bool + raw bool ) func showHelp() bool { @@ -54,8 +58,10 @@ func showHelp() bool { } fmt.Fprintf(os.Stdout, "tile38-cli %s%s\n\n", core.Version, gitsha) fmt.Fprintf(os.Stdout, "Usage: tile38-cli [OPTIONS] [cmd [arg [arg ...]]]\n") - fmt.Fprintf(os.Stdout, " -h Server hostname (default: %s).\n", hostname) - fmt.Fprintf(os.Stdout, " -p Server port (default: %d).\n", port) + fmt.Fprintf(os.Stdout, " --raw Use raw formatting for replies (default when STDOUT is not a tty)\n") + fmt.Fprintf(os.Stdout, " --resp Use RESP output formatting (default is JSON output)\n") + fmt.Fprintf(os.Stdout, " -h Server hostname (default: %s)\n", hostname) + fmt.Fprintf(os.Stdout, " -p Server port (default: %d)\n", port) fmt.Fprintf(os.Stdout, "\n") return false } @@ -82,9 +88,10 @@ func parseArgs() bool { fmt.Fprintf(os.Stderr, "Unrecognized option or bad number of args for: '%s'\n", arg) return false } + for len(args) > 0 { arg := readArg("") - if arg == "--help" { + if arg == "--help" || arg == "-?" { return showHelp() } if !strings.HasPrefix(arg, "-") { @@ -96,6 +103,10 @@ func parseArgs() bool { return badArg(arg) case "-kml": tokml = true + case "--raw": + raw = true + case "--resp": + output = "resp" case "-h": hostname = readArg(arg) case "-p": @@ -120,6 +131,15 @@ func main() { if !parseArgs() { return } + + if !raw { + fi, err := os.Stdout.Stat() + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + return + } + raw = (fi.Mode() & os.ModeCharDevice) == 0 + } if len(oneCommand) > 0 && (oneCommand[0] == 'h' || oneCommand[0] == 'H') && strings.Split(strings.ToLower(oneCommand), " ")[0] == "help" { showHelp() return @@ -222,7 +242,13 @@ func main() { f.Close() } }() - var raw bool + if output == "resp" { + _, err := conn.Do("output resp") + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + return + } + } for { var command string var err error @@ -254,11 +280,6 @@ func main() { if (command[0] == 'q' || command[0] == 'Q') && strings.ToLower(command) == "quit" { return } - if (command[0] == 'r' || command[0] == 'R') && strings.ToLower(command) == "raw" { - raw = true - fmt.Fprintln(os.Stderr, "raw mode is ON") - continue - } if (command[0] == 'h' || command[0] == 'H') && (strings.ToLower(command) == "help" || strings.HasPrefix(strings.ToLower(command), "help")) { err = help(strings.TrimSpace(command[4:])) if err != nil { @@ -266,11 +287,6 @@ func main() { } continue } - if (command[0] == 'p' || command[0] == 'P') && strings.ToLower(command) == "pretty" { - raw = false - fmt.Fprintln(os.Stderr, "raw mode is OFF") - continue - } aof = (command[0] == 'a' || command[0] == 'A') && strings.HasPrefix(strings.ToLower(command), "aof ") msg, err := conn.Do(command) if err != nil { @@ -296,11 +312,18 @@ func main() { if mustOutput { if tokml { msg = convert2kml(msg) - } - if raw { + fmt.Fprintln(os.Stdout, string(msg)) + } else if output == "resp" { + if !raw { + msg = convert2termresp(msg) + } fmt.Fprintln(os.Stdout, string(msg)) } else { - fmt.Fprintln(os.Stdout, string(msg)) + if raw { + fmt.Fprintln(os.Stdout, string(msg)) + } else { + fmt.Fprintln(os.Stdout, string(msg)) + } } } } @@ -315,6 +338,67 @@ func main() { } } +func convert2termresp(msg []byte) []byte { + rd := resp.NewReader(bytes.NewBuffer(msg)) + out := "" + for { + v, _, err := rd.ReadValue() + if err != nil { + break + } + out += convert2termrespval(v, 0) + } + return []byte(strings.TrimSpace(out)) +} + +func convert2termrespval(v resp.Value, spaces int) string { + switch v.Type() { + default: + return v.String() + case resp.BulkString: + if v.IsNull() { + return "(nil)" + } else { + return "\"" + v.String() + "\"" + } + case resp.Integer: + return "(integer) " + v.String() + case resp.Error: + return "(error) " + v.String() + case resp.Array: + arr := v.Array() + if len(arr) == 0 { + return "(empty list or set)" + } + out := "" + nspaces := spaces + numlen(len(arr)) + for i, v := range arr { + if i > 0 { + out += strings.Repeat(" ", spaces) + } + iout := strings.TrimSpace(convert2termrespval(v, nspaces+2)) + out += fmt.Sprintf("%d) %s\n", i+1, iout) + } + return out + } +} + +func numlen(n int) int { + l := 1 + if n < 0 { + l++ + n = n * -1 + } + for i := 0; i < 1000; i++ { + if n < 10 { + break + } + l++ + n = n / 10 + } + return l +} + func convert2kml(msg []byte) []byte { k := NewKML() var m map[string]interface{} diff --git a/controller/controller.go b/controller/controller.go index b0a9a689..105225df 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -190,7 +190,12 @@ func (c *Controller) handleInputCommand(conn *server.Conn, msg *server.Message, _, err = io.WriteString(w, res+"\r\n") return err case server.RESP: - _, err := io.WriteString(w, res) + var err error + if msg.OutputType == server.JSON { + _, err = fmt.Fprintf(w, "$%d\r\n%s\r\n", len(res), res) + } else { + _, err = io.WriteString(w, res) + } return err case server.Native: _, err := fmt.Fprintf(w, "$%d %s\r\n", len(res), res) @@ -278,6 +283,8 @@ func (c *Controller) handleInputCommand(conn *server.Conn, msg *server.Message, // does not write to aof, but requires a write lock. c.mu.Lock() defer c.mu.Unlock() + case "output": + // this is local connection operation. Locks not needed. case "massinsert": // dev operation // ** danger zone ** @@ -340,14 +347,12 @@ func (c *Controller) command(msg *server.Message, w io.Writer) (res string, d co res, d, err = c.cmdDrop(msg) case "flushdb": res, d, err = c.cmdFlushDB(msg) - // case "sethook": - // err = c.cmdSetHook(nline) - // resp = okResp() - // case "delhook": - // err = c.cmdDelHook(nline) - // resp = okResp() - // case "hooks": - // err = c.cmdHooks(nline, w) + case "sethook": + res, d, err = c.cmdSetHook(msg) + case "delhook": + res, d, err = c.cmdDelHook(msg) + case "hooks": + res, err = c.cmdHooks(msg) // case "massinsert": // if !core.DevMode { // err = fmt.Errorf("unknown command '%s'", cmd) @@ -376,6 +381,8 @@ func (c *Controller) command(msg *server.Message, w io.Writer) (res string, d co res, err = c.cmdGet(msg) case "keys": res, err = c.cmdKeys(msg) + case "output": + res, err = c.cmdOutput(msg) // case "aof": // err = c.cmdAOF(nline, w) // case "aofmd5": diff --git a/controller/hooks.go b/controller/hooks.go index 58e03567..b7d259ee 100644 --- a/controller/hooks.go +++ b/controller/hooks.go @@ -4,7 +4,6 @@ import ( "bytes" "errors" "fmt" - "io" "net/http" "net/url" "sort" @@ -42,7 +41,7 @@ type Hook struct { Key string Name string Endpoints []Endpoint - Command string + Message *server.Message Fence *liveFenceSwitches ScanWriter *scanWriter } @@ -156,70 +155,96 @@ func parseEndpoint(s string) (Endpoint, error) { return endpoint, nil } -func (c *Controller) cmdSetHook(line string) (err error) { - //start := time.Now() +func (c *Controller) cmdSetHook(msg *server.Message) (res string, d commandDetailsT, err error) { + start := time.Now() + + vs := msg.Values[1:] var name, values, cmd string - if line, name = token(line); name == "" { - return errInvalidNumberOfArguments + var ok bool + if vs, name, ok = tokenval(vs); !ok || name == "" { + return "", d, errInvalidNumberOfArguments } - if line, values = token(line); values == "" { - return errInvalidNumberOfArguments + if vs, values, ok = tokenval(vs); !ok || values == "" { + return "", d, errInvalidNumberOfArguments } var endpoints []Endpoint for _, value := range strings.Split(values, ",") { endpoint, err := parseEndpoint(value) if err != nil { log.Errorf("sethook: %v", err) - return errInvalidArgument(value) + return "", d, errInvalidArgument(value) } endpoints = append(endpoints, endpoint) } - command := line - if line, cmd = token(line); cmd == "" { - return errInvalidNumberOfArguments + commandvs := vs + if vs, cmd, ok = tokenval(vs); !ok || cmd == "" { + return "", d, errInvalidNumberOfArguments } cmdlc := strings.ToLower(cmd) var types []string switch cmdlc { default: - return errInvalidArgument(cmd) + return "", d, errInvalidArgument(cmd) case "nearby": types = nearbyTypes case "within", "intersects": types = withinOrIntersectsTypes } - var vs []resp.Value - panic("todo: assign vs correctly") s, err := c.cmdSearchArgs(cmdlc, vs, types) if err != nil { - return err + return "", d, err } if !s.fence { - return errors.New("missing FENCE argument") + return "", d, errors.New("missing FENCE argument") } s.cmd = cmdlc + + cmsg := &server.Message{} + *cmsg = *msg + cmsg.Values = commandvs + cmsg.Command = strings.ToLower(cmsg.Values[0].String()) + hook := &Hook{ Key: s.key, Name: name, Endpoints: endpoints, Fence: &s, - Command: command, + Message: cmsg, } var wr bytes.Buffer - var msg *server.Message - panic("todo: cmdSetHook message must be defined") - hook.ScanWriter, err = c.newScanWriter(&wr, msg, s.key, s.output, s.precision, s.glob, s.limit, s.wheres, s.nofields) + hook.ScanWriter, err = c.newScanWriter(&wr, cmsg, s.key, s.output, s.precision, s.glob, s.limit, s.wheres, s.nofields) if err != nil { - return err + return "", d, err } // delete the previous hook if h, ok := c.hooks[name]; ok { + // lets see if the previous hook matches the new hook + if h.Key == hook.Key && h.Name == hook.Name { + if len(h.Endpoints) == len(hook.Endpoints) { + match := true + for i, endpoint := range h.Endpoints { + if endpoint.Original != hook.Endpoints[i].Original { + match = false + break + } + } + if match && resp.ArrayValue(h.Message.Values).Equals(resp.ArrayValue(hook.Message.Values)) { + switch msg.OutputType { + case server.JSON: + return server.OKMessage(msg, start), d, nil + case server.RESP: + return ":0\r\n", d, nil + } + } + } + } if hm, ok := c.hookcols[h.Key]; ok { delete(hm, h.Name) } delete(c.hooks, h.Name) } + d.updated = true c.hooks[name] = hook hm, ok := c.hookcols[hook.Key] if !ok { @@ -227,70 +252,121 @@ func (c *Controller) cmdSetHook(line string) (err error) { c.hookcols[hook.Key] = hm } hm[name] = hook - return nil + switch msg.OutputType { + case server.JSON: + return server.OKMessage(msg, start), d, nil + case server.RESP: + return ":1\r\n", d, nil + } + return "", d, nil } -func (c *Controller) cmdDelHook(line string) (err error) { +func (c *Controller) cmdDelHook(msg *server.Message) (res string, d commandDetailsT, err error) { + start := time.Now() + vs := msg.Values[1:] + var name string - if line, name = token(line); name == "" { - return errInvalidNumberOfArguments + var ok bool + if vs, name, ok = tokenval(vs); !ok || name == "" { + return "", d, errInvalidNumberOfArguments } - if line != "" { - return errInvalidNumberOfArguments + if len(vs) != 0 { + return "", d, errInvalidNumberOfArguments } if h, ok := c.hooks[name]; ok { if hm, ok := c.hookcols[h.Key]; ok { delete(hm, h.Name) } delete(c.hooks, h.Name) + d.updated = true + } + + switch msg.OutputType { + case server.JSON: + return server.OKMessage(msg, start), d, nil + case server.RESP: + if d.updated { + return ":1\r\n", d, nil + } else { + return ":0\r\n", d, nil + } } return } -func (c *Controller) cmdHooks(line string, w io.Writer) (err error) { +func (c *Controller) cmdHooks(msg *server.Message) (res string, err error) { start := time.Now() + vs := msg.Values[1:] var pattern string - if line, pattern = token(line); pattern == "" { - return errInvalidNumberOfArguments + var ok bool + if vs, pattern, ok = tokenval(vs); !ok || pattern == "" { + return "", errInvalidNumberOfArguments } - if line != "" { - return errInvalidNumberOfArguments + if len(vs) != 0 { + return "", errInvalidNumberOfArguments } var hooks []*Hook for name, hook := range c.hooks { - if ok, err := globMatch(pattern, name); err == nil && ok { + match, _ := globMatch(pattern, name) + if match { hooks = append(hooks, hook) - } else if err != nil { - return errInvalidArgument(pattern) } } sort.Sort(hooksByName(hooks)) - buf := &bytes.Buffer{} - buf.WriteString(`{"ok":true,"hooks":[`) - for i, hook := range hooks { - if i > 0 { - buf.WriteByte(',') - } - buf.WriteString(`{`) - buf.WriteString(`"name":` + jsonString(hook.Name)) - buf.WriteString(`,"key":` + jsonString(hook.Key)) - buf.WriteString(`,"endpoints":[`) - for i, endpoint := range hook.Endpoints { + switch msg.OutputType { + case server.JSON: + buf := &bytes.Buffer{} + buf.WriteString(`{"ok":true,"hooks":[`) + for i, hook := range hooks { if i > 0 { buf.WriteByte(',') } - buf.WriteString(jsonString(endpoint.Original)) - } - buf.WriteString(`],"command":` + jsonString(hook.Command)) - buf.WriteString(`}`) - } - buf.WriteString(`],"elapsed":"` + time.Now().Sub(start).String() + "\"}") + buf.WriteString(`{`) + buf.WriteString(`"name":` + jsonString(hook.Name)) + buf.WriteString(`,"key":` + jsonString(hook.Key)) + buf.WriteString(`,"endpoints":[`) + for i, endpoint := range hook.Endpoints { + if i > 0 { + buf.WriteByte(',') + } + buf.WriteString(jsonString(endpoint.Original)) + } + buf.WriteString(`],"command":[`) + for i, v := range hook.Message.Values { + if i > 0 { + buf.WriteString(`,`) + } + buf.WriteString(jsonString(v.String())) + } - w.Write(buf.Bytes()) - return + buf.WriteString(`]}`) + } + buf.WriteString(`],"elapsed":"` + time.Now().Sub(start).String() + "\"}") + return buf.String(), nil + case server.RESP: + var vals []resp.Value + for _, hook := range hooks { + var hvals []resp.Value + hvals = append(hvals, resp.StringValue(hook.Name)) + hvals = append(hvals, resp.StringValue(hook.Key)) + var evals []resp.Value + for _, endpoint := range hook.Endpoints { + evals = append(evals, resp.StringValue(endpoint.Original)) + } + hvals = append(hvals, resp.ArrayValue(evals)) + hvals = append(hvals, resp.ArrayValue(hook.Message.Values)) + vals = append(vals, resp.ArrayValue(hvals)) + } + data, err := resp.ArrayValue(vals).MarshalRESP() + if err != nil { + return "", err + } + return string(data), nil + } + return "", nil } func (c *Controller) sendHTTPMessage(endpoint Endpoint, msg []byte) error { diff --git a/controller/output.go b/controller/output.go new file mode 100644 index 00000000..03a5b31e --- /dev/null +++ b/controller/output.go @@ -0,0 +1,40 @@ +package controller + +import ( + "strings" + "time" + + "github.com/tidwall/tile38/controller/server" +) + +func (c *Controller) cmdOutput(msg *server.Message) (res string, err error) { + start := time.Now() + vs := msg.Values[1:] + var arg string + var ok bool + if len(vs) != 0 { + if vs, arg, ok = tokenval(vs); !ok || arg == "" { + return "", errInvalidNumberOfArguments + } + // Setting the original message output type will be picked up by the + // server prior to the next command being executed. + switch strings.ToLower(arg) { + default: + return "", errInvalidArgument(arg) + case "json": + msg.OutputType = server.JSON + case "resp": + msg.OutputType = server.RESP + } + return server.OKMessage(msg, start), nil + } + // return the output + switch msg.OutputType { + default: + return "", nil + case server.JSON: + return `{"ok":true,"output":"json","elapsed":` + time.Now().Sub(start).String() + `}`, nil + case server.RESP: + return "$4\r\nresp\r\n", nil + } +} diff --git a/controller/server/server.go b/controller/server/server.go index fb1c3e26..a1157dd9 100644 --- a/controller/server/server.go +++ b/controller/server/server.go @@ -98,6 +98,7 @@ func handleConn( } } defer conn.Close() + outputType := Null rd := NewAnyReaderWriter(conn) brd := rd.rd for { @@ -114,6 +115,9 @@ func handleConn( return } if msg != nil && msg.Command != "" { + if outputType != Null { + msg.OutputType = outputType + } if msg.Command == "quit" { if msg.OutputType == RESP { io.WriteString(conn, "+OK\r\n") @@ -125,6 +129,7 @@ func handleConn( log.Error(err) return } + outputType = msg.OutputType } else { conn.Write([]byte("HTTP/1.1 500 Bad Request\r\nConnection: close\r\n\r\n")) return @@ -132,6 +137,7 @@ func handleConn( if msg.ConnType == HTTP || msg.ConnType == WebSocket { return } + } }