vm/cli: redo the whole thing using abiosoft/ishell

Adds history support, better editing and way better help system. Expand on
some helps while at it.
This commit is contained in:
Roman Khimov 2019-09-10 19:11:48 +03:00
parent d51ab52a5e
commit e872b6b421
3 changed files with 285 additions and 163 deletions

View file

@ -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 <ip>
<ip> 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 <file>
<file> 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 <string>
<string> is mandatory parameter, example:
> load 006166`,
Func: handleLoadHex,
},
{
Name: "loadgo",
Help: "Compile and load a Go file into the VM",
LongHelp: `Usage: loadhex <file>
<file> is mandatory parameter, example:
> load /path/to/file.go`,
Func: handleLoadGo,
},
{
Name: "run",
Help: "Execute the current loaded script",
LongHelp: `Usage: run [<operation>] [<parameter>...]
<operation> is an operation name, passed as a first parameter to Main() (and it
can't be 'help' at the moment)
<parameter> is a parameter (can be repeated multiple times) specified
as <type>:<value>, 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 [<n>]
<n> 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 <ip>"))
}
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 := `
_ ____________ __________ _ ____ ___