From cc60708cf4c612110f3e3775f5a66f606acc6124 Mon Sep 17 00:00:00 2001 From: crazy2be Date: Wed, 4 Apr 2012 10:32:47 -0400 Subject: [PATCH] More simple readline functionality added to wfdr/moduled. Still have to add cursor movement and unit tests. Not sure what terminals this will work on. --- moduled/escapeseq.go | 11 +++ moduled/modules.go | 25 ------ moduled/rpc.go | 46 ++++++++++- moduled/shell.go | 190 +++++++++++++++++++++++++++++++++++++++++-- moduled/stty.go | 45 ++++++++++ wfdr/wfdr.go | 29 +++++-- 6 files changed, 308 insertions(+), 38 deletions(-) create mode 100644 moduled/escapeseq.go delete mode 100644 moduled/modules.go create mode 100644 moduled/stty.go diff --git a/moduled/escapeseq.go b/moduled/escapeseq.go new file mode 100644 index 0000000..f4ff8c7 --- /dev/null +++ b/moduled/escapeseq.go @@ -0,0 +1,11 @@ +package moduled + +type Seq byte + +var ( + SEQ_NONE = Seq(0) + SEQ_UP = Seq('A') + SEQ_DOWN = Seq('B') + SEQ_RIGHT = Seq('C') + SEQ_LEFT = Seq('D') +) \ No newline at end of file diff --git a/moduled/modules.go b/moduled/modules.go deleted file mode 100644 index 2cc4609..0000000 --- a/moduled/modules.go +++ /dev/null @@ -1,25 +0,0 @@ -package moduled - -import ( - "errors" - "net/rpc" -) - -// StartModule tells the rpc server given by conn to stop the module given by name. -func StartModule(conn *rpc.Client, name string) error { - var dummy int = 1000 - err := conn.Call("ModuleSrv.Start", &name, &dummy) - if err != nil { - return errors.New("Error starting " + name + ":" + err.Error()) - } - return nil -} - -func StopModule(conn *rpc.Client, name string) error { - var dummy int = 1000 - err := conn.Call("ModuleSrv.Stop", &name, &dummy) - if err != nil { - return errors.New("Error stopping " + name + ":" + err.Error()) - } - return nil -} \ No newline at end of file diff --git a/moduled/rpc.go b/moduled/rpc.go index 36f00e4..7d9af5b 100644 --- a/moduled/rpc.go +++ b/moduled/rpc.go @@ -6,8 +6,12 @@ import ( "net/rpc/jsonrpc" ) +type Conn struct { + client *rpc.Client +} + // Connects to the pipe files, in order to allow this program to sent commands to the process management deamon. -func RPCConnect() (*rpc.Client, error) { +func RPCConnect() (*Conn, error) { // Pipes are reversed from what you would expect because we are connecting as a client, and they are named based on how the server uses them. Thus, the out pipe for the server is the in pipe for us. outpipe := "cache/wfdr-deamon-pipe-in" inpipe := "cache/wfdr-deamon-pipe-out" @@ -23,5 +27,43 @@ func RPCConnect() (*rpc.Client, error) { rwc := &PipeReadWriteCloser{Input: infile, Output: outfile} - return jsonrpc.NewClient(rwc), nil + return &Conn{client: jsonrpc.NewClient(rwc)}, nil +} + +func (c *Conn) Start(name string) error { + var dummy int = 1000 + err := c.client.Call("ModuleSrv.Start", &name, &dummy) + if err != nil { + return errors.New("Error starting " + name + ": " + err.Error()) + } + return nil +} + +func (c *Conn) Stop(name string) error { + var dummy int = 1000 + err := c.client.Call("ModuleSrv.Stop", &name, &dummy) + if err != nil { + return errors.New("Error stopping " + name + ": " + err.Error()) + } + return nil } + +func (c *Conn) Restart(name string) error { + err := c.Stop(name) + if err != nil { + return err + } + return c.Start(name) +} + +func (c *Conn) Status(name string) (running bool, err error) { + err = c.client.Call("ModuleSrv.Status", &name, &running) + if err != nil { + return false, errors.New("Error getting status for " + name + ": " + err.Error()) + } + return running, nil +} + +func (c *Conn) Close() error { + return c.client.Close() +} \ No newline at end of file diff --git a/moduled/shell.go b/moduled/shell.go index c176a14..07b727f 100644 --- a/moduled/shell.go +++ b/moduled/shell.go @@ -1,17 +1,195 @@ package moduled import ( - "net/rpc" + "errors" + "bufio" + "fmt" "io" ) type Shell struct { - conn *rpc.Client - rd io.Reader + hist [][]byte + histpos int + + // Buffer of characters on the current line + linebuf []byte + // Current position of the cursor + pos int + + // Where to read commands from + rd *bufio.Reader + wr io.Writer } -func NewShell(conn *rpc.Client, rd io.Reader) { +func NewShell(rd io.Reader, wr io.Writer) *Shell { s := new(Shell) - s.conn = conn - s.rd = rd + s.rd = bufio.NewReader(rd) + s.wr = wr + return s +} + +func (s *Shell) parseTokens() ([]string, error) { + tokens := make([]string, 0) + buf := make([]byte, 0) + + for _, b := range s.linebuf { + switch b { + case ' ', '\t': + if buf != nil { + tokens = append(tokens, string(buf)) + buf = nil + } + default: + buf = append(buf, b) + } + } + tokens = append(tokens, string(buf)) + return tokens, nil +} + +// ReadCommand presents the user with an interactive prompt where they can enter a command, and includes facilities for backspace, and history. Returns the parsed command string (command and arguments), and an error, if any. +func (s *Shell) ReadCommand() ([]string, error) { + s.linebuf = nil + s.histpos = -1 + for { + b, err := s.rd.ReadByte() + if err != nil { + return nil, err + } + + switch b { + case '\n': + if s.histpos == -1 { + s.hist = append(s.hist, s.linebuf) + } else { + s.hist[len(s.hist) - 1] = s.linebuf + } + return s.parseTokens() + case 127: // Backspace (Actually DEL) + s.bksp(3) + if len(s.linebuf) == 0 || s.linebuf == nil { + break + } + s.linebuf = s.linebuf[:len(s.linebuf)-1] + case 27: // ESC + seq, err := s.readEscSeq() + if err != nil { + return nil, err + } + err = s.handleEscSeq(seq) + if err != nil { + return nil, err + } + default: + s.linebuf = append(s.linebuf, b) + } + } + panic("not reached!") +} + +func (s *Shell) readEscSeq() (Seq, error) { + b, err := s.rd.ReadByte() + if err != nil { + return SEQ_NONE, err + } + if b != '[' { + return SEQ_NONE, errors.New("Expected '[' after ESC") + } + + pmc := make([]byte, 0) + for { + b, err = s.rd.ReadByte() + if err != nil { + return SEQ_NONE, err + } + if (b < 22) { + return SEQ_NONE, fmt.Errorf("Unexpected character %d ('%c')", b, b) + } else if (b <= 47) { + pmc = append(pmc, b) + } else if (b <= 57) { + // Number + return SEQ_NONE, fmt.Errorf("Numbers in escape sequences not yet supported!") + } else if (b <= 63) { + return SEQ_NONE, fmt.Errorf("Unexpected character %d ('%c')", b, b) + } else if (b <= 126) { + return Seq(b), nil + } else { + return SEQ_NONE, fmt.Errorf("Unexpected character %d ('%c')", b, b) + } + } + panic("Not reached!") +} + +func (s *Shell) bksp(num int) { + for i := 0; i < num; i++ { + fmt.Fprintf(s.wr, "%c %c", 8, 8) + } +} + +func (s *Shell) handleEscSeq(seq Seq) error { + if seq == SEQ_UP || seq == SEQ_DOWN { + if s.histpos == -1 { + s.hist = append(s.hist, s.linebuf) + s.histpos = len(s.hist)-1 + } + } + switch (seq) { + case SEQ_UP: + s.histpos-- + if s.histpos < 0 { + s.histpos = 0 + s.bksp(4) + return nil + } + case SEQ_DOWN: + s.histpos++ + if s.histpos >= len(s.hist) - 1 { + s.histpos = len(s.hist) - 1 + s.bksp(4) + return nil + } + default: + fmt.Printf("Read escape sequence of %d", seq) + return fmt.Errorf("Read unsupported escape sequence of %d ('%c')", seq, seq) + } + if seq == SEQ_UP || seq == SEQ_DOWN { + s.bksp(len(s.linebuf) + 4) + s.linebuf = s.hist[s.histpos] + fmt.Fprintf(s.wr, "%s", s.linebuf) + } + return nil +} + +func (c *Conn) InterpretCommand(args []string) error { + if args == nil || len(args) < 1 { + return errors.New("No command provided!") + } + cmd := args[0] + args = args[1:] + + switch (cmd) { + case "start": + for _, module := range args { + err := c.Start(module) + if err != nil { + return err + } + } + case "stop": + for _, module := range args { + if len(args) < 1 { + return errors.New("Module name required!") + } + return c.Stop(module) + } + case "restart": + for _, module := range args { + if len(args) < 1 { + return errors.New("Module name required!") + } + return c.Restart(module) + } + } + + return nil } \ No newline at end of file diff --git a/moduled/stty.go b/moduled/stty.go new file mode 100644 index 0000000..58e184b --- /dev/null +++ b/moduled/stty.go @@ -0,0 +1,45 @@ +package moduled + +import ( + "os" + "log" + "os/exec" + "errors" +) + +type sttyState string + +func (stty sttyState) Undo() { + proc := exec.Command("stty", string(stty)) + proc.Stdin = os.Stdin + err := proc.Run() + if err != nil { + log.Println("Warning: Failed to reset terminal options with to " + stty) + return + } + return +} + +func readStty() (sttyState, error) { + proc := exec.Command("/bin/stty", "-g") + proc.Stdin = os.Stdin + output, err := proc.Output() + if err != nil { + return sttyState(output), errors.New("Reading tty state with 'stty -g': " + err.Error()) + } + return sttyState(output), nil +} + +func SttyCbreak() (sttyState, error) { + state, err := readStty() + if err != nil { + return state, err + } + proc := exec.Command("/bin/stty", "cbreak") + proc.Stdin = os.Stdin + err = proc.Run() + if err != nil { + return state, errors.New("Executing 'stty raw': " + err.Error()) + } + return state, nil +} \ No newline at end of file diff --git a/wfdr/wfdr.go b/wfdr/wfdr.go index ba50520..939a993 100644 --- a/wfdr/wfdr.go +++ b/wfdr/wfdr.go @@ -8,11 +8,31 @@ import ( "log" "os" "os/exec" - // Local imports + "wfdr/moduled" + +// "github.com/kylelemons/goat" ) func main() { +// buf := make([]byte, 3) + state, err := moduled.SttyCbreak() + defer state.Undo() + if err != nil { + log.Println(state) + log.Fatal(err) + } + s := moduled.NewShell(os.Stdin, os.Stdout) + for { + cmd, err := s.ReadCommand() + if err != nil { + fmt.Println("Error while reading command: ", err) + } + if cmd != nil { + fmt.Printf("Read command: %#v\n", cmd) + } + } + os.Exit(0) if len(os.Args) < 2 { printHelp() os.Exit(0) @@ -82,13 +102,13 @@ func moduleAction(module, action string) { log.Fatal(err) } if action == "stop" || action == "restart" { - err = moduled.StopModule(client, module) + err = client.Stop(module) } if err != nil { break } if action == "start" || action == "restart" { - err = moduled.StartModule(client, module) + err = client.Start(module) } case "compile": mustRun("wfdr-compile", module) @@ -107,8 +127,7 @@ func moduleAction(module, action string) { } for _, fi := range fis { name := fi.Name() - var running bool - err = client.Call("ModuleSrv.Status", &name, &running) + running, err := client.Status(name) if err != nil { log.Fatal(err) }