From 972eb004d2dfa1886301dfc52ee7af8ff51a3321 Mon Sep 17 00:00:00 2001
From: fancl <fancl@justcall.cn>
Date: Thu, 18 Jan 2024 17:11:44 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96cli=E5=91=BD=E4=BB=A4?=
 =?UTF-8?q?=E8=A1=8C=E7=9A=84=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 cmd/main.go            | 20 +++++++++++++++++++-
 entry/cli/client.go    | 10 +++++-----
 entry/cli/context.go   | 15 +++++++++++++--
 entry/cli/frame.go     | 10 +++++++++-
 entry/cli/router.go    |  3 +++
 entry/cli/serialize.go |  2 +-
 entry/cli/server.go    | 17 ++++++++++++++---
 options.go             | 20 ++++++++++++++++++++
 service.go             |  9 +++++----
 types.go               |  4 ++--
 10 files changed, 91 insertions(+), 19 deletions(-)

diff --git a/cmd/main.go b/cmd/main.go
index e6e186f..30c377f 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"embed"
 	"flag"
+	"git.nspix.com/golang/kos/entry/cli"
 	"git.nspix.com/golang/kos/entry/http"
 	httpkg "net/http"
 
@@ -16,11 +17,29 @@ var webDir embed.FS
 type subServer struct {
 }
 
+type users struct {
+	Name string   `json:"name"`
+	Age  int      `json:"age"`
+	Tags []string `json:"tags"`
+}
+
 func (s *subServer) Start(ctx context.Context) (err error) {
 	kos.Http().Root("/web", httpkg.FS(webDir))
 	kos.Http().Handle(httpkg.MethodGet, "/hello", func(ctx *http.Context) (err error) {
 		return ctx.Success("Hello World")
 	})
+	kos.Command().Handle("/test", "test command", func(ctx *cli.Context) (err error) {
+		return ctx.Success([][]string{
+			[]string{"NAME", "AGE"},
+			[]string{"SSS", "aaa"},
+		})
+	})
+	kos.Command().Handle("/users", "test command", func(ctx *cli.Context) (err error) {
+		return ctx.Success([]*users{
+			{Name: "Zhan", Age: 10, Tags: []string{"a", "b"}},
+			{Name: "Lisi", Age: 15, Tags: []string{"c", "d"}},
+		})
+	})
 	return
 }
 
@@ -33,7 +52,6 @@ func main() {
 	svr := kos.Init(
 		kos.WithName("git.nspix.com/golang/test", "0.0.1"),
 		kos.WithServer(&subServer{}),
-		kos.WithDirectHttp(),
 	)
 	svr.Run()
 }
diff --git a/entry/cli/client.go b/entry/cli/client.go
index ad41936..bfbabf2 100644
--- a/entry/cli/client.go
+++ b/entry/cli/client.go
@@ -141,7 +141,7 @@ func (client *Client) completer(str string) (ss []string) {
 	)
 	ss = make([]string, 0)
 	seq = client.getSequence()
-	if err = writeFrame(client.conn, newFrame(PacketTypeCompleter, FlagComplete, seq, []byte(str))); err != nil {
+	if err = writeFrame(client.conn, newFrame(PacketTypeCompleter, FlagComplete, seq, client.Timeout, []byte(str))); err != nil {
 		return
 	}
 	select {
@@ -166,10 +166,10 @@ func (client *Client) Execute(s string) (err error) {
 	}()
 	go client.ioLoop(client.conn)
 	seq = client.getSequence()
-	if err = writeFrame(client.conn, newFrame(PacketTypeCommand, FlagComplete, seq, []byte(s))); err != nil {
+	if err = writeFrame(client.conn, newFrame(PacketTypeCommand, FlagComplete, seq, client.Timeout, []byte(s))); err != nil {
 		return err
 	}
-	client.waitResponse(seq, time.Second*30)
+	client.waitResponse(seq, client.Timeout)
 	return
 }
 
@@ -185,7 +185,7 @@ func (client *Client) Shell() (err error) {
 	defer func() {
 		_ = client.Close()
 	}()
-	if err = writeFrame(client.conn, newFrame(PacketTypeHandshake, FlagComplete, client.getSequence(), nil)); err != nil {
+	if err = writeFrame(client.conn, newFrame(PacketTypeHandshake, FlagComplete, client.getSequence(), client.Timeout, nil)); err != nil {
 		return
 	}
 	go client.ioLoop(client.conn)
@@ -216,7 +216,7 @@ func (client *Client) Shell() (err error) {
 			continue
 		}
 		seq = client.getSequence()
-		if err = writeFrame(client.conn, newFrame(PacketTypeCommand, FlagComplete, seq, []byte(line))); err != nil {
+		if err = writeFrame(client.conn, newFrame(PacketTypeCommand, FlagComplete, seq, client.Timeout, []byte(line))); err != nil {
 			break
 		}
 		client.liner.AppendHistory(line)
diff --git a/entry/cli/context.go b/entry/cli/context.go
index d4ec067..a04bc9b 100644
--- a/entry/cli/context.go
+++ b/entry/cli/context.go
@@ -1,6 +1,7 @@
 package cli
 
 import (
+	"context"
 	"fmt"
 	"io"
 	"math"
@@ -9,6 +10,7 @@ import (
 type Context struct {
 	Id     int64
 	seq    uint16
+	ctx    context.Context
 	wc     io.WriteCloser
 	params map[string]string
 	args   []string
@@ -18,6 +20,7 @@ func (ctx *Context) reset(id int64, wc io.WriteCloser) {
 	ctx.Id = id
 	ctx.wc = wc
 	ctx.seq = 0
+	ctx.ctx = context.Background()
 	ctx.args = make([]string, 0)
 	ctx.params = make(map[string]string)
 }
@@ -34,6 +37,14 @@ func (ctx *Context) Bind(v any) (err error) {
 	return
 }
 
+func (ctx *Context) setContext(c context.Context) {
+	ctx.ctx = c
+}
+
+func (ctx *Context) Context() context.Context {
+	return ctx.Context()
+}
+
 func (ctx *Context) Argument(index int) string {
 	if index >= len(ctx.args) || index < 0 {
 		return ""
@@ -93,12 +104,12 @@ __END:
 	chunkSize := math.MaxInt16 - 1
 	n := len(buf) / chunkSize
 	for i := 0; i < n; i++ {
-		if err = writeFrame(ctx.wc, newFrame(res.Type, FlagPortion, ctx.seq, buf[offset:chunkSize+offset])); err != nil {
+		if err = writeFrame(ctx.wc, newFrame(res.Type, FlagPortion, ctx.seq, 0, buf[offset:chunkSize+offset])); err != nil {
 			return
 		}
 		offset += chunkSize
 	}
-	err = writeFrame(ctx.wc, newFrame(res.Type, FlagComplete, ctx.seq, buf[offset:]))
+	err = writeFrame(ctx.wc, newFrame(res.Type, FlagComplete, ctx.seq, 0, buf[offset:]))
 	return
 }
 
diff --git a/entry/cli/frame.go b/entry/cli/frame.go
index 8a1ac5f..bc0b451 100644
--- a/entry/cli/frame.go
+++ b/entry/cli/frame.go
@@ -27,6 +27,7 @@ type (
 		Seq       uint16 `json:"seq"`
 		Data      []byte `json:"data"`
 		Error     string `json:"error"`
+		Timeout   int64  `json:"timeout"`
 		Timestamp int64  `json:"timestamp"`
 	}
 )
@@ -55,6 +56,9 @@ func readFrame(r io.Reader) (frame *Frame, err error) {
 	if err = binary.Read(r, binary.LittleEndian, &frame.Seq); err != nil {
 		return
 	}
+	if err = binary.Read(r, binary.LittleEndian, &frame.Timeout); err != nil {
+		return
+	}
 	if err = binary.Read(r, binary.LittleEndian, &frame.Timestamp); err != nil {
 		return
 	}
@@ -116,6 +120,9 @@ func writeFrame(w io.Writer, frame *Frame) (err error) {
 	if err = binary.Write(w, binary.LittleEndian, frame.Seq); err != nil {
 		return
 	}
+	if err = binary.Write(w, binary.LittleEndian, frame.Timeout); err != nil {
+		return
+	}
 	if err = binary.Write(w, binary.LittleEndian, frame.Timestamp); err != nil {
 		return
 	}
@@ -142,13 +149,14 @@ func writeFrame(w io.Writer, frame *Frame) (err error) {
 	return
 }
 
-func newFrame(t, f byte, seq uint16, data []byte) *Frame {
+func newFrame(t, f byte, seq uint16, timeout time.Duration, data []byte) *Frame {
 	return &Frame{
 		Feature:   Feature,
 		Type:      t,
 		Flag:      f,
 		Seq:       seq,
 		Data:      data,
+		Timeout:   int64(timeout),
 		Timestamp: time.Now().Unix(),
 	}
 }
diff --git a/entry/cli/router.go b/entry/cli/router.go
index e0c4fb4..b5cc35a 100644
--- a/entry/cli/router.go
+++ b/entry/cli/router.go
@@ -97,6 +97,9 @@ func (r *Router) Handle(path string, command Command) {
 		name = path
 		path = ""
 	}
+	if name == "-" {
+		name = "app"
+	}
 	children := r.getChildren(name)
 	if children == nil {
 		children = newRouter(name)
diff --git a/entry/cli/serialize.go b/entry/cli/serialize.go
index d6636fe..6842ad3 100644
--- a/entry/cli/serialize.go
+++ b/entry/cli/serialize.go
@@ -170,7 +170,7 @@ func serializeArray(val []any) (buf []byte, err error) {
 			if rv.Kind() == reflect.Array || rv.Kind() == reflect.Slice {
 				row := make([]any, 0, rv.Len())
 				for i := 0; i < rv.Len(); i++ {
-					if isNormalKind(rv.Index(i).Elem().Kind()) || rv.Index(i).Interface() == nil {
+					if isNormalKind(rv.Index(i).Kind()) || rv.Index(i).Interface() == nil {
 						row = append(row, rv.Index(i).Interface())
 					} else {
 						goto __END
diff --git a/entry/cli/server.go b/entry/cli/server.go
index 42804e2..dad4630 100644
--- a/entry/cli/server.go
+++ b/entry/cli/server.go
@@ -44,9 +44,8 @@ func (svr *Server) releaseContext(ctx *Context) {
 	ctxPool.Put(ctx)
 }
 
-func (svr *Server) handle(ctx *Context, frame *Frame) {
+func (svr *Server) handle(ctx *Context, frame *Frame) (err error) {
 	var (
-		err    error
 		params map[string]string
 		tokens []string
 		args   []string
@@ -54,6 +53,15 @@ func (svr *Server) handle(ctx *Context, frame *Frame) {
 	)
 	cmd := string(frame.Data)
 	tokens = strings.Fields(cmd)
+	if frame.Timeout > 0 {
+		childCtx, cancelFunc := context.WithTimeout(svr.ctx, time.Duration(frame.Timeout))
+		ctx.setContext(childCtx)
+		defer func() {
+			cancelFunc()
+		}()
+	} else {
+		ctx.setContext(svr.ctx)
+	}
 	if r, args, err = svr.router.Lookup(tokens); err != nil {
 		if errors.Is(err, ErrNotFound) {
 			err = ctx.Error(errNotFound, fmt.Sprintf("Command %s not found", cmd))
@@ -75,6 +83,7 @@ func (svr *Server) handle(ctx *Context, frame *Frame) {
 		ctx.setParam(params)
 		err = r.command.Handle(ctx)
 	}
+	return
 }
 
 func (svr *Server) nextSequence() int64 {
@@ -130,7 +139,9 @@ func (svr *Server) process(conn net.Conn) {
 				break
 			}
 		case PacketTypeCommand:
-			svr.handle(ctx, frame)
+			if err = svr.handle(ctx, frame); err != nil {
+				break
+			}
 		default:
 			break
 		}
diff --git a/options.go b/options.go
index b2f78d9..1f2bc7d 100644
--- a/options.go
+++ b/options.go
@@ -31,6 +31,12 @@ type (
 	}
 
 	Option func(o *Options)
+
+	HandleOptions struct {
+		description string
+	}
+
+	HandleOption func(o *HandleOptions)
 )
 
 func (o *Options) ShortName() string {
@@ -45,6 +51,12 @@ func (o *Options) ShortName() string {
 	return o.shortName
 }
 
+func WithHandleDescription(s string) HandleOption {
+	return func(o *HandleOptions) {
+		o.description = s
+	}
+}
+
 func WithName(name string, version string) Option {
 	return func(o *Options) {
 		o.Name = name
@@ -100,3 +112,11 @@ func NewOptions(cbs ...Option) *Options {
 	}
 	return opts
 }
+
+func newHandleOptions(cbs ...HandleOption) *HandleOptions {
+	opts := &HandleOptions{}
+	for _, cb := range cbs {
+		cb(opts)
+	}
+	return opts
+}
diff --git a/service.go b/service.go
index 10b2892..e8ffcff 100644
--- a/service.go
+++ b/service.go
@@ -76,14 +76,15 @@ func (app *application) Command() *cli.Server {
 	return app.command
 }
 
-func (app *application) Handle(path string, cb HandleFunc) {
+func (app *application) Handle(path string, cb HandleFunc, cbs ...HandleOption) {
+	opts := newHandleOptions(cbs...)
 	if app.http != nil {
 		app.http.Handle(http.MethodPost, path, func(ctx *http.Context) (err error) {
 			return cb(ctx)
 		})
 	}
 	if app.command != nil {
-		app.command.Handle(path, "", func(ctx *cli.Context) (err error) {
+		app.command.Handle(path, opts.description, func(ctx *cli.Context) (err error) {
 			return cb(ctx)
 		})
 	}
@@ -252,10 +253,10 @@ func (app *application) preStart() (err error) {
 				Uptime:  time.Now().Sub(app.uptime).String(),
 				Gateway: app.gateway.State(),
 			})
-		})
+		}, WithHandleDescription("Display application state"))
 		app.Handle("/-/healthy", func(ctx Context) (err error) {
 			return ctx.Success(app.Healthy())
-		})
+		}, WithHandleDescription("Display application healthy"))
 	}
 	app.plugins.Range(func(key, value any) bool {
 		if plugin, ok := value.(Plugin); ok {
diff --git a/types.go b/types.go
index ae3daec..15323cc 100644
--- a/types.go
+++ b/types.go
@@ -22,9 +22,9 @@ type (
 		Info() *Info
 		Http() *http.Server
 		Command() *cli.Server
-		Handle(method string, cb HandleFunc)
+		Handle(method string, cb HandleFunc, opts ...HandleOption)
 	}
-	
+
 	// Info application information
 	Info struct {
 		ID         string            `json:"id"`