Initial import.

This commit is contained in:
2021-04-19 13:10:40 -07:00
commit 42aa585bd0
16 changed files with 863 additions and 0 deletions

101
cps/cps.go Normal file
View File

@@ -0,0 +1,101 @@
// Package cps provides the command processing system.
package cps
import (
"context"
"errors"
"fmt"
"strings"
)
type Command struct {
Context context.Context
Raw string // The original command passed to the agent.
Command string // Which command was invoked?
Arg string // Input string sans the leading command.
Args []string // Tokenised list of arguments.
Meta map[string]string // Sometimes you need additional information.
}
func (cmd *Command) Handle() (*Response, error) {
return handle(cmd)
}
// Parse takes a command string from an input string and parent context.
func Parse(ctx context.Context, line string) (*Command, error) {
line = strings.TrimSpace(line)
args := strings.Fields(line)
if len(args) == 0 {
return nil, errors.New("cps: no command line provided")
}
cmd := &Command{
Context: ctx,
Raw: line,
Command: args[0],
Args: args[1:],
Meta: map[string]string{},
}
cmd.Arg = strings.TrimSpace(strings.TrimPrefix(cmd.Raw, cmd.Command))
return cmd, nil
}
// ParseBackground calls Parse(context.Background, line).
func ParseBackground(line string) (*Command, error) {
return Parse(context.Background(), line)
}
type Response struct {
Message string
Error error
}
func (r *Response) String() string {
if r == nil {
return "carrier signal lost"
}
out := ""
if r.Message != "" {
out += r.Message
} else if r.Error != nil {
out = fmt.Sprintf("Error: %s", r.Error)
}
return out
}
var registry = map[string]func(*Command) (*Response, error){}
func Register(command string, handler func(*Command) (*Response, error)) {
if _, ok := registry[command]; ok {
panic("handler for " + command + " already registered!")
}
registry[command] = handler
}
func handle(cmd *Command) (*Response, error) {
handler, ok := registry[cmd.Command]
if !ok {
err := fmt.Errorf("cps: no handler for command '%s'", cmd.Command)
return &Response{
Message: fmt.Sprintf("%s is not a recognized command", cmd.Command),
Error: err,
}, err
}
return handler(cmd)
}
func Handle(ctx context.Context, line string) (*Response, error) {
cmd, err := Parse(ctx, line)
if err != nil {
return &Response{
Message: "Unable to process message",
Error: err,
}, err
}
return cmd.Handle()
}

103
cps/cps_test.go Normal file
View File

@@ -0,0 +1,103 @@
package cps
import (
"context"
"testing"
)
type logger interface {
Log(args ...interface{})
Logf(format string, args ...interface{})
}
func (cmd *Command) cmp(o *Command, l logger) bool {
if cmd.Raw != o.Raw {
l.Logf("mismatched raw: have '%s', want '%s'", o.Raw, cmd.Raw)
return false
}
if cmd.Command != o.Command {
l.Logf("mismatched command: have '%s', want '%s'", o.Command, cmd.Command)
return false
}
if cmd.Arg != o.Arg {
l.Logf("mismatched arg: have '%s', want '%s'", o.Arg, cmd.Arg)
return false
}
if len(cmd.Args) != len(o.Args) {
l.Logf("mismatched arglen: have %d, want %d", len(o.Args), len(cmd.Args))
return false
}
for i := range cmd.Args {
if cmd.Args[i] != o.Args[i] {
l.Logf("mismatched arg %d: have '%s', want '%s'", o.Args[i], cmd.Args[i])
return false
}
}
if len(cmd.Meta) != len(o.Meta) {
l.Logf("mismatched metalen: have %d, want %d", len(o.Meta), len(cmd.Meta))
return false
}
for k := range cmd.Meta {
if cmd.Meta[k] != o.Meta[k] {
l.Logf("mismatched meta for key '%s': have '%s', want '%s'",
k, o.Meta[k], cmd.Meta[k])
return false
}
}
return true
}
func TestParseEmptyCommand(t *testing.T) {
inputLine := "hello"
expected := &Command{
Context: context.Background(),
Raw: "hello",
Command: "hello",
Arg: "",
Args: []string{},
Meta: map[string]string{},
}
cmd, err := ParseBackground(inputLine)
if err != nil {
t.Fatal(err)
}
if !expected.cmp(cmd, t) {
t.FailNow()
}
}
func TestParseNotEmptyCommand(t *testing.T) {
inputLine := "hello world this is a test"
expected := &Command{
Context: context.Background(),
Raw: "hello world this is a test",
Command: "hello",
Arg: "world this is a test",
Args: []string{
"world",
"this",
"is",
"a",
"test",
},
Meta: map[string]string{},
}
cmd, err := ParseBackground(inputLine)
if err != nil {
t.Fatal(err)
}
if !expected.cmp(cmd, t) {
t.FailNow()
}
}

22
cps/help.go Normal file
View File

@@ -0,0 +1,22 @@
package cps
import (
"sort"
"strings"
)
func init() {
Register("help", helpHandler)
}
func helpHandler(cmd *Command) (*Response, error) {
knownCommands := make([]string, 0, len(registry))
for command := range registry {
knownCommands = append(knownCommands, command)
}
sort.Strings(knownCommands)
return &Response{
Message: strings.Join(knownCommands, ", "),
}, nil
}

17
cps/ping.go Normal file
View File

@@ -0,0 +1,17 @@
package cps
func init() {
Register("ping", pingHandler)
}
// Ping just returns pong and is used to verify that the system is alive.
func pingHandler(cmd *Command) (*Response, error) {
message := "pong"
if len(cmd.Args) > 0 {
message += " (FYI, no additional args are used)"
}
return &Response{
Message: message,
}, nil
}