diff --git a/go.mod b/go.mod index db667f4df..b59552d03 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,19 @@ module github.com/CityOfZion/neo-go require ( + github.com/abiosoft/ishell v2.0.0+incompatible // indirect + github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 // indirect github.com/alicebob/miniredis v2.5.0+incompatible github.com/davecgh/go-spew v1.1.0 // indirect + github.com/fatih/color v1.7.0 // indirect + github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect github.com/go-redis/redis v6.10.2+incompatible github.com/go-yaml/yaml v2.1.0+incompatible github.com/golang/snappy v0.0.0-20170215233205-553a64147049 // indirect github.com/gomodule/redigo v2.0.0+incompatible // indirect + github.com/mattn/go-colorable v0.1.2 // indirect + github.com/mattn/go-isatty v0.0.9 // indirect github.com/mr-tron/base58 v1.1.2 github.com/nspcc-dev/rfc6979 v0.1.0 github.com/onsi/gomega v1.4.2 // indirect @@ -21,6 +27,7 @@ require ( golang.org/x/crypto v0.0.0-20180316180149-374053ea96cb golang.org/x/text v0.3.0 golang.org/x/tools v0.0.0-20180318012157-96caea41033d + gopkg.in/abiosoft/ishell.v2 v2.0.0 gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect ) diff --git a/go.sum b/go.sum index 564580acc..9e5af119e 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,22 @@ +github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw= +github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg= +github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8= +github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530= github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 h1:45bxf7AZMwWcqkLzDAQugVEwedisr5nRJ1r+7LYnv0U= github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI= github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI= +github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-redis/redis v6.10.2+incompatible h1:SLbqrO/Ik1nhXA5/cbEs1P5MUBo1Qq4ihlNfGnnipPw= @@ -21,6 +31,11 @@ github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNu github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mr-tron/base58 v1.1.2 h1:ZEw4I2EgPKDJ2iEw0cNmLB3ROrEmkOtXIkaG7wZg+78= github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/nspcc-dev/rfc6979 v0.1.0 h1:Lwg7esRRoyK1Up/IN1vAef1EmvrBeMHeeEkek2fAJ6c= @@ -51,12 +66,16 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6Zh golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190204203706-41f3e6584952 h1:FDfvYgoVsA7TTZSbgiqjAbfPbK47CNHdWl3h/PJtii0= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20180318012157-96caea41033d h1:Xmo0nLTRYewf0eXDvo12nMSuOgNQ4283hdbOHIUf7h8= golang.org/x/tools v0.0.0-20180318012157-96caea41033d/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/abiosoft/ishell.v2 v2.0.0 h1:/J5yh3nWYSSGFjALcitTI9CLE0Tu27vBYHX0srotqOc= +gopkg.in/abiosoft/ishell.v2 v2.0.0/go.mod h1:sFp+cGtH6o4s1FtpVPTMcHq2yue+c4DGOVohJCPUzwY= gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/pkg/vm/cli/cli.go b/pkg/vm/cli/cli.go index 84bc8ebcd..78f9fd6df 100644 --- a/pkg/vm/cli/cli.go +++ b/pkg/vm/cli/cli.go @@ -1,197 +1,312 @@ package cli import ( - "bufio" "bytes" "encoding/hex" "errors" "fmt" "io/ioutil" "os" - "sort" "strconv" "strings" - "text/tabwriter" "github.com/CityOfZion/neo-go/pkg/vm" "github.com/CityOfZion/neo-go/pkg/vm/compiler" + "gopkg.in/abiosoft/ishell.v2" ) -// command describes a VM command. -type command struct { - // number of minimum arguments the command needs. - args int - // description of the command. - usage string - // whether the VM needs to be "ready" to execute this command. - ready bool -} +const vmKey = "vm" -var commands = map[string]command{ - "help": {0, "show available commands", false}, - "exit": {0, "exit the VM prompt", false}, - "ip": {0, "show the current instruction", true}, - "break": {1, "place a breakpoint (> break 1)", true}, - "estack": {0, "show evaluation stack details", false}, - "astack": {0, "show alt stack details", false}, - "istack": {0, "show invocation stack details", false}, - "loadavm": {1, "load an avm script into the VM (> load /path/to/script.avm)", false}, - "loadhex": {1, "load a hex string into the VM (> loadhex 006166 )", false}, - "loadgo": {1, "compile and load a .go file into the VM (> load /path/to/file.go)", false}, - "run": {0, "execute the current loaded script", true}, - "cont": {0, "continue execution of the current loaded script", true}, - "step": {0, "step (n) instruction in the program (> step 10)", true}, - "ops": {0, "show the opcodes of the current loaded program", true}, +var commands = []*ishell.Cmd{ + { + Name: "exit", + Help: "Exit the VM prompt", + LongHelp: "Exit the VM prompt", + Func: handleExit, + }, + { + Name: "ip", + Help: "Show current instruction", + LongHelp: "Show current instruction", + Func: handleIP, + }, + { + Name: "break", + Help: "Place a breakpoint", + LongHelp: `Usage: break + is mandatory parameter, example: +> break 12`, + Func: handleBreak, + }, + { + Name: "estack", + Help: "Show evaluation stack contents", + LongHelp: "Show evaluation stack contents", + Func: handleXStack, + }, + { + Name: "astack", + Help: "Show alt stack contents", + LongHelp: "Show alt stack contents", + Func: handleXStack, + }, + { + Name: "istack", + Help: "Show invocation stack contents", + LongHelp: "Show invocation stack contents", + Func: handleXStack, + }, + { + Name: "loadavm", + Help: "Load an avm script into the VM", + LongHelp: `Usage: loadavm + is mandatory parameter, example: +> load /path/to/script.avm`, + Func: handleLoadAVM, + }, + { + Name: "loadhex", + Help: "Load a hex-encoded script string into the VM", + LongHelp: `Usage: loadhex + is mandatory parameter, example: +> load 006166`, + Func: handleLoadHex, + }, + { + Name: "loadgo", + Help: "Compile and load a Go file into the VM", + LongHelp: `Usage: loadhex + is mandatory parameter, example: +> load /path/to/file.go`, + Func: handleLoadGo, + }, + { + Name: "run", + Help: "Execute the current loaded script", + LongHelp: `Usage: run [] [...] + + is an operation name, passed as a first parameter to Main() (and it + can't be 'help' at the moment) + is a parameter (can be repeated multiple times) specified + as :, where type can be 'int' or 'string' and value is + a value of this type (string is pushed as a byte array value) + +Parameters are packed into array before they're passed to the script. so +effectively 'run' only supports contracts with signatures like this: + func Main(operation string, args []interface{}) interface{} + +Example: +> run put string:"Something to put"`, + Func: handleRun, + }, + { + Name: "cont", + Help: "Continue execution of the current loaded script", + LongHelp: "Continue execution of the current loaded script", + Func: handleCont, + }, + { + Name: "step", + Help: "Step (n) instruction in the program", + LongHelp: `Usage: step [] + is optional parameter to specify number of instructions to run, example: +> step 10`, + Func: handleStep, + }, + { + Name: "ops", + Help: "Dump opcodes of the current loaded program", + LongHelp: "Dump opcodes of the current loaded program", + Func: handleOps, + }, } // VMCLI object for interacting with the VM. type VMCLI struct { - vm *vm.VM + vm *vm.VM + shell *ishell.Shell } // New returns a new VMCLI object. func New() *VMCLI { - return &VMCLI{ - vm: vm.New(0), + vmcli := VMCLI{ + vm: vm.New(0), + shell: ishell.New(), } + vmcli.shell.Set(vmKey, vmcli.vm) + for _, c := range commands { + vmcli.shell.AddCmd(c) + } + changePrompt(vmcli.shell, vmcli.vm) + return &vmcli } -func (c *VMCLI) handleCommand(cmd string, args ...string) { - com, ok := commands[cmd] - if !ok { - fmt.Printf("unknown command (%s)\n", cmd) +func getVMFromContext(c *ishell.Context) *vm.VM { + return c.Get(vmKey).(*vm.VM) +} + +func checkVMIsReady(c *ishell.Context) bool { + v := getVMFromContext(c) + if v == nil || !v.Ready() { + c.Err(errors.New("VM is not ready: no program loaded")) + return false + } + return true +} + +func handleExit(c *ishell.Context) { + c.Println("Bye!") + os.Exit(0) +} + +func handleIP(c *ishell.Context) { + if !checkVMIsReady(c) { return } - if (len(args) < com.args || len(args) > com.args) && cmd != "run" { - fmt.Printf("command (%s) takes at least %d arguments\n", cmd, com.args) + v := getVMFromContext(c) + ip, opcode := v.Context().CurrInstr() + c.Printf("instruction pointer at %d (%s)\n", ip, opcode) +} + +func handleBreak(c *ishell.Context) { + if !checkVMIsReady(c) { return } - if com.ready && !c.vm.Ready() { - fmt.Println("VM is not ready: no program loaded") + v := getVMFromContext(c) + if len(c.Args) != 1 { + c.Err(errors.New("Missing parameter ")) + } + n, err := strconv.Atoi(c.Args[0]) + if err != nil { + c.Err(fmt.Errorf("argument conversion error: %s", err)) return } - switch cmd { - case "help": - printHelp() + v.AddBreakPoint(n) + c.Printf("breakpoint added at instruction %d\n", n) +} - case "exit": - fmt.Println("Bye!") - os.Exit(0) +func handleXStack(c *ishell.Context) { + v := getVMFromContext(c) + c.Println(v.Stack(c.Cmd.Name)) +} - case "ip": - ip, opcode := c.vm.Context().CurrInstr() - fmt.Printf("instruction pointer at %d (%s)\n", ip, opcode) +func handleLoadAVM(c *ishell.Context) { + v := getVMFromContext(c) + if err := v.LoadFile(c.Args[0]); err != nil { + c.Err(err) + } else { + c.Printf("READY: loaded %d instructions\n", v.Context().LenInstr()) + } + changePrompt(c, v) +} - case "break": - n, err := strconv.Atoi(args[0]) - if err != nil { - fmt.Printf("argument conversion error: %s\n", err) - return - } +func handleLoadHex(c *ishell.Context) { + v := getVMFromContext(c) + b, err := hex.DecodeString(c.Args[0]) + if err != nil { + c.Err(err) + return + } + v.Load(b) + c.Printf("READY: loaded %d instructions\n", v.Context().LenInstr()) + changePrompt(c, v) +} - c.vm.AddBreakPoint(n) - fmt.Printf("breakpoint added at instruction %d\n", n) +func handleLoadGo(c *ishell.Context) { + v := getVMFromContext(c) + fb, err := ioutil.ReadFile(c.Args[0]) + if err != nil { + c.Err(err) + return + } + b, err := compiler.Compile(bytes.NewReader(fb), &compiler.Options{}) + if err != nil { + c.Err(err) + return + } - case "estack", "istack", "astack": - fmt.Println(c.vm.Stack(cmd)) + v.Load(b) + c.Printf("READY: loaded %d instructions\n", v.Context().LenInstr()) + changePrompt(c, v) +} - case "loadavm": - if err := c.vm.LoadFile(args[0]); err != nil { - fmt.Println(err) - } else { - fmt.Printf("READY: loaded %d instructions\n", c.vm.Context().LenInstr()) - } - - case "loadhex": - b, err := hex.DecodeString(args[0]) - if err != nil { - fmt.Println(err) - return - } - c.vm.Load(b) - fmt.Printf("READY: loaded %d instructions\n", c.vm.Context().LenInstr()) - - case "loadgo": - fb, err := ioutil.ReadFile(args[0]) - if err != nil { - fmt.Println(err) - return - } - b, err := compiler.Compile(bytes.NewReader(fb), &compiler.Options{}) - if err != nil { - fmt.Println(err) - return - } - c.vm.Load(b) - fmt.Printf("READY: loaded %d instructions\n", c.vm.Context().LenInstr()) - - case "run": - if len(args) != 0 { - var ( - method []byte - params []vm.StackItem - err error - start int - ) - if isMethodArg(args[0]) { - method = []byte(args[0]) - start = 1 - } - params, err = parseArgs(args[start:]) - if err != nil { - fmt.Println(err) - return - } - c.vm.LoadArgs(method, params) - } - c.vm.Run() - - case "cont": - c.vm.Run() - - case "step": +func handleRun(c *ishell.Context) { + v := getVMFromContext(c) + if len(c.Args) != 0 { var ( - n = 1 - err error + method []byte + params []vm.StackItem + err error + start int ) - if len(args) > 0 { - n, err = strconv.Atoi(args[0]) - if err != nil { - fmt.Printf("argument conversion error: %s\n", err) - return - } + if isMethodArg(c.Args[0]) { + method = []byte(c.Args[0]) + start = 1 } - c.vm.AddBreakPointRel(n) - c.vm.Run() + params, err = parseArgs(c.Args[start:]) + if err != nil { + c.Err(err) + return + } + v.LoadArgs(method, params) + } + v.Run() + changePrompt(c, v) +} - case "ops": - c.vm.PrintOps() +func handleCont(c *ishell.Context) { + if !checkVMIsReady(c) { + return + } + v := getVMFromContext(c) + v.Run() + changePrompt(c, v) +} + +func handleStep(c *ishell.Context) { + var ( + n = 1 + err error + ) + + if !checkVMIsReady(c) { + return + } + v := getVMFromContext(c) + if len(c.Args) > 0 { + n, err = strconv.Atoi(c.Args[0]) + if err != nil { + c.Err(fmt.Errorf("argument conversion error: %s", err)) + return + } + } + v.AddBreakPointRel(n) + v.Run() + changePrompt(c, v) +} + +func handleOps(c *ishell.Context) { + if !checkVMIsReady(c) { + return + } + v := getVMFromContext(c) + v.PrintOps() +} + +func changePrompt(c ishell.Actions, v *vm.VM) { + if v.Ready() && v.Context().IP()-1 >= 0 { + c.SetPrompt(fmt.Sprintf("NEO-GO-VM %d > ", v.Context().IP()-1)) + } else { + c.SetPrompt("NEO-GO-VM > ") } } // Run waits for user input from Stdin and executes the passed command. func (c *VMCLI) Run() error { printLogo() - reader := bufio.NewReader(os.Stdin) - for { - if c.vm.Ready() && c.vm.Context().IP()-1 >= 0 { - fmt.Printf("NEO-GO-VM %d > ", c.vm.Context().IP()-1) - } else { - fmt.Print("NEO-GO-VM > ") - } - input, _ := reader.ReadString('\n') - input = strings.Trim(input, "\n") - if len(input) != 0 { - parts := strings.Split(input, " ") - cmd := parts[0] - var args []string - if len(parts) > 1 { - args = parts[1:] - } - c.handleCommand(cmd, args...) - } - } + c.shell.Run() + return nil } func isMethodArg(s string) bool { @@ -224,25 +339,6 @@ func parseArgs(args []string) ([]vm.StackItem, error) { return items, nil } -func printHelp() { - names := make([]string, len(commands)) - i := 0 - for name := range commands { - names[i] = name - i++ - } - sort.Strings(names) - - fmt.Println() - w := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0) - fmt.Fprintln(w, "COMMAND\tUSAGE") - for _, name := range names { - fmt.Fprintf(w, "%s\t%s\n", name, commands[name].usage) - } - w.Flush() - fmt.Println() -} - func printLogo() { logo := ` _ ____________ __________ _ ____ ___