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"`