diff --git a/cli/vm/vm.go b/cli/vm/vm.go index 3bb725828..50993e970 100644 --- a/cli/vm/vm.go +++ b/cli/vm/vm.go @@ -3,7 +3,7 @@ package vm import ( "os" - "github.com/abiosoft/readline" + "github.com/chzyer/readline" vmcli "github.com/nspcc-dev/neo-go/pkg/vm/cli" "github.com/urfave/cli" ) @@ -21,9 +21,6 @@ func NewCommands() []cli.Command { } func startVMPrompt(ctx *cli.Context) error { - p := vmcli.NewWithConfig(true, os.Exit, &readline.Config{ - Stdout: ctx.App.Writer, - Stderr: ctx.App.ErrWriter, - }) + p := vmcli.NewWithConfig(true, os.Exit, &readline.Config{}) return p.Run() } diff --git a/cli/wallet_test.go b/cli/wallet_test.go index 504ffe3a9..762221c34 100644 --- a/cli/wallet_test.go +++ b/cli/wallet_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "github.com/abiosoft/readline" + "github.com/chzyer/readline" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" diff --git a/go.mod b/go.mod index ec84a1455..2c5c7e4be 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,13 @@ module github.com/nspcc-dev/neo-go require ( - github.com/abiosoft/ishell/v2 v2.0.2 - github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db github.com/btcsuite/btcd v0.22.0-beta + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/davecgh/go-spew v1.1.1 github.com/gorilla/websocket v1.4.2 github.com/hashicorp/golang-lru v0.5.4 github.com/holiman/uint256 v1.2.0 + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/mr-tron/base58 v1.2.0 github.com/nspcc-dev/dbft v0.0.0-20210721160347-1b03241391ac github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 diff --git a/go.sum b/go.sum index 3023f9c2b..3806d2704 100644 --- a/go.sum +++ b/go.sum @@ -6,11 +6,8 @@ github.com/CityOfZion/neo-go v0.70.1-pre.0.20191209120015-fccb0085941e/go.mod h1 github.com/CityOfZion/neo-go v0.70.1-pre.0.20191212173117-32ac01130d4c/go.mod h1:JtlHfeqLywZLswKIKFnAp+yzezY4Dji9qlfQKB2OD/I= github.com/CityOfZion/neo-go v0.71.1-pre.0.20200129171427-f773ec69fb84/go.mod h1:FLI526IrRWHmcsO+mHsCbj64pJZhwQFTLJZu+A4PGOA= github.com/Workiva/go-datastructures v1.0.50/go.mod h1:Z+F2Rca0qCsVYDS8z7bAGm8f3UkzuWYS/oBZz5a7VVA= -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/ishell/v2 v2.0.2 h1:5qVfGiQISaYM8TkbBl7RFO6MddABoXpATrsFbVI+SNo= github.com/abiosoft/ishell/v2 v2.0.2/go.mod h1:E4oTCXfo6QjoCart0QYa5m9w4S+deXs/P/9jA77A9Bs= -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/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -47,6 +44,7 @@ github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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 h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 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= @@ -70,9 +68,7 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go. github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -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/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= @@ -142,6 +138,8 @@ github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/ github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -155,12 +153,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= diff --git a/pkg/vm/cli/cli.go b/pkg/vm/cli/cli.go index 5b4878804..d37da180a 100644 --- a/pkg/vm/cli/cli.go +++ b/pkg/vm/cli/cli.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "io/ioutil" "math/big" "os" @@ -15,9 +16,10 @@ import ( "strings" "text/tabwriter" - "github.com/abiosoft/ishell/v2" - "github.com/abiosoft/readline" + "github.com/chzyer/readline" + "github.com/kballard/go-shellquote" "github.com/nspcc-dev/neo-go/pkg/compiler" + "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/encoding/bigint" @@ -27,119 +29,136 @@ import ( "github.com/nspcc-dev/neo-go/pkg/util/slice" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/urfave/cli" ) const ( - vmKey = "vm" - manifestKey = "manifest" - boolType = "bool" - boolFalse = "false" - boolTrue = "true" - intType = "int" - stringType = "string" - exitFunc = "exitFunc" + vmKey = "vm" + manifestKey = "manifest" + exitFuncKey = "exitFunc" + readlineInstanceKey = "readlineKey" + printLogoKey = "printLogoKey" + boolType = "bool" + boolFalse = "false" + boolTrue = "true" + intType = "int" + stringType = "string" ) -var commands = []*ishell.Cmd{ +var commands = []cli.Command{ { - Name: "exit", - Help: "Exit the VM prompt", - LongHelp: "Exit the VM prompt", - Func: handleExit, + Name: "exit", + Usage: "Exit the VM prompt", + Description: "Exit the VM prompt", + Action: handleExit, }, { - Name: "ip", - Help: "Show current instruction", - LongHelp: "Show current instruction", - Func: handleIP, + Name: "ip", + Usage: "Show current instruction", + Description: "Show current instruction", + Action: handleIP, }, { - Name: "break", - Help: "Place a breakpoint", - LongHelp: `Usage: break + Name: "break", + Usage: "Place a breakpoint", + UsageText: `break `, + Description: `break is mandatory parameter, example: > break 12`, - Func: handleBreak, + Action: handleBreak, }, { - Name: "estack", - Help: "Show evaluation stack contents", - LongHelp: "Show evaluation stack contents", - Func: handleXStack, + Name: "estack", + Usage: "Show evaluation stack contents", + Description: "Show evaluation stack contents", + Action: handleXStack, }, { - Name: "istack", - Help: "Show invocation stack contents", - LongHelp: "Show invocation stack contents", - Func: handleXStack, + Name: "istack", + Usage: "Show invocation stack contents", + Description: "Show invocation stack contents", + Action: handleXStack, }, { - Name: "sslot", - Help: "Show static slot contents", - LongHelp: "Show static slot contents", - Func: handleSlots, + Name: "sslot", + Usage: "Show static slot contents", + Description: "Show static slot contents", + Action: handleSlots, }, { - Name: "lslot", - Help: "Show local slot contents", - LongHelp: "Show local slot contents", - Func: handleSlots, + Name: "lslot", + Usage: "Show local slot contents", + Description: "Show local slot contents", + Action: handleSlots, }, { - Name: "aslot", - Help: "Show arguments slot contents", - LongHelp: "Show arguments slot contents", - Func: handleSlots, + Name: "aslot", + Usage: "Show arguments slot contents", + Description: "Show arguments slot contents", + Action: handleSlots, }, { - Name: "loadnef", - Help: "Load a NEF-consistent script into the VM", - LongHelp: `Usage: loadnef + Name: "loadnef", + Usage: "Load a NEF-consistent script into the VM", + UsageText: `loadnef `, + Description: `loadnef both parameters are mandatory, example: > loadnef /path/to/script.nef /path/to/manifest.json`, - Func: handleLoadNEF, + Action: handleLoadNEF, }, { - Name: "loadbase64", - Help: "Load a base64-encoded script string into the VM", - LongHelp: `Usage: loadbase64 + Name: "loadbase64", + Usage: "Load a base64-encoded script string into the VM", + UsageText: `loadbase64 `, + Description: `loadbase64 + is mandatory parameter, example: > loadbase64 AwAQpdToAAAADBQV9ehtQR1OrVZVhtHtoUHRfoE+agwUzmFvf3Rhfg/EuAVYOvJgKiON9j8TwAwIdHJhbnNmZXIMFDt9NxHG8Mz5sdypA9G/odiW8SOMQWJ9W1I4`, - Func: handleLoadBase64, + Action: handleLoadBase64, }, { - Name: "loadhex", - Help: "Load a hex-encoded script string into the VM", - LongHelp: `Usage: loadhex + Name: "loadhex", + Usage: "Load a hex-encoded script string into the VM", + UsageText: `loadhex `, + Description: `loadhex + is mandatory parameter, example: > loadhex 0c0c48656c6c6f20776f726c6421`, - Func: handleLoadHex, + Action: handleLoadHex, }, { - Name: "loadgo", - Help: "Compile and load a Go file with the manifest into the VM", - LongHelp: `Usage: loadgo + Name: "loadgo", + Usage: "Compile and load a Go file with the manifest into the VM", + UsageText: `loadgo `, + Description: `loadgo + is mandatory parameter, example: > loadgo /path/to/file.go`, - Func: handleLoadGo, + Action: handleLoadGo, }, { - Name: "parse", - Help: "Parse provided argument and convert it into other possible formats", - LongHelp: `Usage: parse + Name: "reset", + Usage: "Unload compiled script from the VM", + Action: handleReset, + }, + { + Name: "parse", + Usage: "Parse provided argument and convert it into other possible formats", + UsageText: `parse `, + Description: `parse is an argument which is tried to be interpreted as an item of different types - and converted to other formats. Strings are escaped and output in quotes.`, - Func: handleParse, +and converted to other formats. Strings are escaped and output in quotes.`, + Action: handleParse, }, { - Name: "run", - Help: "Execute the current loaded script", - LongHelp: `Usage: run [ [...]] + Name: "run", + Usage: "Execute the current loaded script", + UsageText: `run [ [...]]`, + Description: `run [ [...]] - is a contract method, specified in manifest (and it - can't be 'help' at the moment). It can be '_' which will push parameters - onto the stack and execute from the current offset. + is a contract method, specified in manifest. It can be '_' which will push + parameters onto the stack and execute from the current offset. is a parameter (can be repeated multiple times) that can be specified as :, where type can be: '` + boolType + `': supports '` + boolFalse + `' and '` + boolTrue + `' values @@ -153,54 +172,72 @@ both parameters are mandatory, example: Example: > run put ` + stringType + `:"Something to put"`, - Func: handleRun, + Action: handleRun, }, { - Name: "cont", - Help: "Continue execution of the current loaded script", - LongHelp: "Continue execution of the current loaded script", - Func: handleCont, + Name: "cont", + Usage: "Continue execution of the current loaded script", + Description: "Continue execution of the current loaded script", + Action: handleCont, }, { - Name: "step", - Help: "Step (n) instruction in the program", - LongHelp: `Usage: step [] + Name: "step", + Usage: "Step (n) instruction in the program", + UsageText: `step []`, + Description: `step [] is optional parameter to specify number of instructions to run, example: > step 10`, - Func: handleStep, + Action: handleStep, }, { - Name: "stepinto", - Help: "Stepinto instruction to take in the debugger", - LongHelp: `Usage: stepInto + Name: "stepinto", + Usage: "Stepinto instruction to take in the debugger", + Description: `Usage: stepInto example: > stepinto`, - Func: handleStepInto, + Action: handleStepInto, }, { - Name: "stepout", - Help: "Stepout instruction to take in the debugger", - LongHelp: `Usage: stepOut + Name: "stepout", + Usage: "Stepout instruction to take in the debugger", + Description: `stepOut example: > stepout`, - Func: handleStepOut, + Action: handleStepOut, }, { - Name: "stepover", - Help: "Stepover instruction to take in the debugger", - LongHelp: `Usage: stepOver + Name: "stepover", + Usage: "Stepover instruction to take in the debugger", + Description: `stepOver example: > stepover`, - Func: handleStepOver, + Action: handleStepOver, }, { - Name: "ops", - Help: "Dump opcodes of the current loaded program", - LongHelp: "Dump opcodes of the current loaded program", - Func: handleOps, + Name: "ops", + Usage: "Dump opcodes of the current loaded program", + Description: "Dump opcodes of the current loaded program", + Action: handleOps, }, } +var completer *readline.PrefixCompleter + +func init() { + var pcItems []readline.PrefixCompleterInterface + for _, c := range commands { + if !c.Hidden { + var flagsItems []readline.PrefixCompleterInterface + for _, f := range c.Flags { + names := strings.SplitN(f.GetName(), ", ", 2) // only long name will be offered + flagsItems = append(flagsItems, readline.PcItem("--"+names[0])) + } + pcItems = append(pcItems, readline.PcItem(c.Name, flagsItems...)) + } + } + completer = readline.NewPrefixCompleter(pcItems...) +} + // Various errors. var ( ErrMissingParameter = errors.New("missing argument") @@ -210,119 +247,165 @@ var ( // VMCLI object for interacting with the VM. type VMCLI struct { vm *vm.VM - shell *ishell.Shell - // printLogo specifies if logo is printed. - printLogo bool + shell *cli.App } // New returns a new VMCLI object. func New() *VMCLI { return NewWithConfig(true, os.Exit, &readline.Config{ - Prompt: ">>>", + Prompt: "\033[32mNEO-GO-VM >\033[0m ", // green prompt ^^ }) } // NewWithConfig returns new VMCLI instance using provided config. -func NewWithConfig(printLogo bool, onExit func(int), c *readline.Config) *VMCLI { +func NewWithConfig(printLogotype bool, onExit func(int), c *readline.Config) *VMCLI { + if c.AutoComplete == nil { + // Autocomplete commands/flags on TAB. + c.AutoComplete = completer + } + l, err := readline.NewEx(c) + if err != nil { + panic(err) + } + ctl := cli.NewApp() + ctl.Name = "VM CLI" + + // Note: need to set empty `ctl.HelpName` and `ctl.UsageText`, otherwise + // `filepath.Base(os.Args[0])` will be used which is `neo-go`. + ctl.HelpName = "" + ctl.UsageText = "" + + ctl.Writer = l.Stdout() + ctl.ErrWriter = l.Stderr() + ctl.Version = config.Version + ctl.Usage = "Official VM CLI for Neo-Go" + + // Override default error handler in order not to exit on error. + ctl.ExitErrHandler = func(context *cli.Context, err error) {} + + ctl.Commands = commands + vmcli := VMCLI{ - vm: vm.New(), - shell: ishell.NewWithConfig(c), - printLogo: printLogo, + vm: vm.New(), + shell: ctl, } - vmcli.shell.Set(vmKey, vmcli.vm) - vmcli.shell.Set(manifestKey, new(manifest.Manifest)) - vmcli.shell.Set(exitFunc, onExit) - for _, c := range commands { - vmcli.shell.AddCmd(c) + + vmcli.shell.Metadata = map[string]interface{}{ + vmKey: vmcli.vm, + manifestKey: new(manifest.Manifest), + exitFuncKey: onExit, + readlineInstanceKey: l, + printLogoKey: printLogotype, } - changePrompt(vmcli.shell, vmcli.vm) + changePrompt(vmcli.shell) return &vmcli } -func getVMFromContext(c *ishell.Context) *vm.VM { - return c.Get(vmKey).(*vm.VM) +func getExitFuncFromContext(app *cli.App) func(int) { + return app.Metadata[exitFuncKey].(func(int)) } -func getManifestFromContext(c *ishell.Context) *manifest.Manifest { - return c.Get(manifestKey).(*manifest.Manifest) +func getReadlineInstanceFromContext(app *cli.App) *readline.Instance { + return app.Metadata[readlineInstanceKey].(*readline.Instance) } -func setManifestInContext(c *ishell.Context, m *manifest.Manifest) { - old := getManifestFromContext(c) +func getVMFromContext(app *cli.App) *vm.VM { + return app.Metadata[vmKey].(*vm.VM) +} + +func setVMInContext(app *cli.App, v *vm.VM) { + old := getVMFromContext(app) + *old = *v +} + +func getManifestFromContext(app *cli.App) *manifest.Manifest { + return app.Metadata[manifestKey].(*manifest.Manifest) +} + +func getPrintLogoFromContext(app *cli.App) bool { + return app.Metadata[printLogoKey].(bool) +} + +func setManifestInContext(app *cli.App, m *manifest.Manifest) { + old := getManifestFromContext(app) *old = *m } -func checkVMIsReady(c *ishell.Context) bool { - v := getVMFromContext(c) +func checkVMIsReady(app *cli.App) bool { + v := getVMFromContext(app) if v == nil || !v.Ready() { - c.Err(errors.New("VM is not ready: no program loaded")) + writeErr(app.Writer, errors.New("VM is not ready: no program loaded")) return false } return true } -func handleExit(c *ishell.Context) { - c.Println("Bye!") - c.Get(exitFunc).(func(int))(0) +func handleExit(c *cli.Context) error { + l := getReadlineInstanceFromContext(c.App) + _ = l.Close() + exit := getExitFuncFromContext(c.App) + fmt.Fprintln(c.App.Writer, "Bye!") + exit(0) + return nil } -func handleIP(c *ishell.Context) { - if !checkVMIsReady(c) { - return +func handleIP(c *cli.Context) error { + if !checkVMIsReady(c.App) { + return nil } - v := getVMFromContext(c) + v := getVMFromContext(c.App) ctx := v.Context() if ctx.NextIP() < ctx.LenInstr() { ip, opcode := v.Context().NextInstr() - c.Printf("instruction pointer at %d (%s)\n", ip, opcode) + fmt.Fprintf(c.App.Writer, "instruction pointer at %d (%s)\n", ip, opcode) } else { - c.Println("execution has finished") + fmt.Fprintln(c.App.Writer, "execution has finished") } + return nil } -func handleBreak(c *ishell.Context) { - if !checkVMIsReady(c) { - return +func handleBreak(c *cli.Context) error { + if !checkVMIsReady(c.App) { + return nil } - v := getVMFromContext(c) - if len(c.Args) != 1 { - c.Err(fmt.Errorf("%w: ", ErrMissingParameter)) - return + v := getVMFromContext(c.App) + args := c.Args() + if len(args) != 1 { + return fmt.Errorf("%w: ", ErrMissingParameter) } - n, err := strconv.Atoi(c.Args[0]) + n, err := strconv.Atoi(args[0]) if err != nil { - c.Err(fmt.Errorf("%w: %v", ErrInvalidParameter, err)) - return + return fmt.Errorf("%w: %s", ErrInvalidParameter, err) } v.AddBreakPoint(n) - c.Printf("breakpoint added at instruction %d\n", n) + fmt.Fprintf(c.App.Writer, "breakpoint added at instruction %d\n", n) + return nil } -func handleXStack(c *ishell.Context) { - v := getVMFromContext(c) +func handleXStack(c *cli.Context) error { + v := getVMFromContext(c.App) var stackDump string - switch c.Cmd.Name { + switch c.Command.Name { case "estack": stackDump = v.DumpEStack() case "istack": stackDump = v.DumpIStack() default: - c.Err(errors.New("unknown stack")) - return + return errors.New("unknown stack") } - c.Println(stackDump) + fmt.Fprintln(c.App.Writer, stackDump) + return nil } -func handleSlots(c *ishell.Context) { - v := getVMFromContext(c) +func handleSlots(c *cli.Context) error { + v := getVMFromContext(c.App) vmCtx := v.Context() if vmCtx == nil { - c.Err(errors.New("no program loaded")) - return + return errors.New("no program loaded") } var rawSlot string - switch c.Cmd.Name { + switch c.Command.Name { case "sslot": rawSlot = vmCtx.DumpStaticSlot() case "lslot": @@ -330,89 +413,93 @@ func handleSlots(c *ishell.Context) { case "aslot": rawSlot = vmCtx.DumpArgumentsSlot() default: - c.Err(errors.New("unknown slot")) - return + return errors.New("unknown slot") } - c.Println(rawSlot) + fmt.Fprintln(c.App.Writer, rawSlot) + return nil } -func handleLoadNEF(c *ishell.Context) { - v := getVMFromContext(c) - if len(c.Args) < 2 { - c.Err(fmt.Errorf("%w: ", ErrMissingParameter)) - return +func handleLoadNEF(c *cli.Context) error { + v := getVMFromContext(c.App) + args := c.Args() + if len(args) < 2 { + return fmt.Errorf("%w: ", ErrMissingParameter) } - if err := v.LoadFileWithFlags(c.Args[0], callflag.All); err != nil { - c.Err(err) - return + if err := v.LoadFileWithFlags(args[0], callflag.All); err != nil { + return fmt.Errorf("failed to read nef: %w", err) } - m, err := getManifestFromFile(c.Args[1]) + m, err := getManifestFromFile(args[1]) if err != nil { - c.Err(err) - return + return fmt.Errorf("failed to read manifest: %w", err) } - c.Printf("READY: loaded %d instructions\n", v.Context().LenInstr()) - setManifestInContext(c, m) - changePrompt(c, v) + fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr()) + setManifestInContext(c.App, m) + changePrompt(c.App) + return nil } -func handleLoadBase64(c *ishell.Context) { - v := getVMFromContext(c) - if len(c.Args) < 1 { - c.Err(fmt.Errorf("%w: ", ErrMissingParameter)) - return +func handleLoadBase64(c *cli.Context) error { + v := getVMFromContext(c.App) + args := c.Args() + if len(args) < 1 { + return fmt.Errorf("%w: ", ErrMissingParameter) } - b, err := base64.StdEncoding.DecodeString(c.Args[0]) + b, err := base64.StdEncoding.DecodeString(args[0]) if err != nil { - c.Err(fmt.Errorf("%w: %v", ErrInvalidParameter, err)) - return + return fmt.Errorf("%w: %s", ErrInvalidParameter, err) } v.LoadWithFlags(b, callflag.All) - c.Printf("READY: loaded %d instructions\n", v.Context().LenInstr()) - changePrompt(c, v) + fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr()) + changePrompt(c.App) + return nil } -func handleLoadHex(c *ishell.Context) { - v := getVMFromContext(c) - if len(c.Args) < 1 { - c.Err(fmt.Errorf("%w: ", ErrMissingParameter)) - return +func handleLoadHex(c *cli.Context) error { + v := getVMFromContext(c.App) + args := c.Args() + if len(args) < 1 { + return fmt.Errorf("%w: ", ErrMissingParameter) } - b, err := hex.DecodeString(c.Args[0]) + b, err := hex.DecodeString(args[0]) if err != nil { - c.Err(fmt.Errorf("%w: %v", ErrInvalidParameter, err)) - return + return fmt.Errorf("%w: %s", ErrInvalidParameter, err) } v.LoadWithFlags(b, callflag.All) - c.Printf("READY: loaded %d instructions\n", v.Context().LenInstr()) - changePrompt(c, v) + fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr()) + changePrompt(c.App) + return nil } -func handleLoadGo(c *ishell.Context) { - v := getVMFromContext(c) - if len(c.Args) < 1 { - c.Err(fmt.Errorf("%w: ", ErrMissingParameter)) - return +func handleLoadGo(c *cli.Context) error { + v := getVMFromContext(c.App) + args := c.Args() + if len(args) < 1 { + return fmt.Errorf("%w: ", ErrMissingParameter) } - name := strings.TrimSuffix(c.Args[0], ".go") - b, di, err := compiler.CompileWithOptions(c.Args[0], nil, &compiler.Options{Name: name}) + name := strings.TrimSuffix(args[0], ".go") + b, di, err := compiler.CompileWithOptions(args[0], nil, &compiler.Options{Name: name}) if err != nil { - c.Err(err) - return + return fmt.Errorf("failed to compile: %w", err) } // Don't perform checks, just load. m, err := di.ConvertToManifest(&compiler.Options{}) if err != nil { - c.Err(fmt.Errorf("can't create manifest: %w", err)) - return + return fmt.Errorf("can't create manifest: %w", err) } - setManifestInContext(c, m) + setManifestInContext(c.App, m) v.LoadWithFlags(b.Script, callflag.All) - c.Printf("READY: loaded %d instructions\n", v.Context().LenInstr()) - changePrompt(c, v) + fmt.Fprintf(c.App.Writer, "READY: loaded %d instructions\n", v.Context().LenInstr()) + changePrompt(c.App) + return nil +} + +func handleReset(c *cli.Context) error { + setVMInContext(c.App, vm.New()) + changePrompt(c.App) + return nil } func getManifestFromFile(name string) (*manifest.Manifest, error) { @@ -428,27 +515,26 @@ func getManifestFromFile(name string) (*manifest.Manifest, error) { return &m, nil } -func handleRun(c *ishell.Context) { - v := getVMFromContext(c) - m := getManifestFromContext(c) - if len(c.Args) != 0 { +func handleRun(c *cli.Context) error { + v := getVMFromContext(c.App) + m := getManifestFromContext(c.App) + args := c.Args() + if len(args) != 0 { var ( params []stackitem.Item offset int err error - runCurrent = c.Args[0] != "_" + runCurrent = args[0] != "_" ) - params, err = parseArgs(c.Args[1:]) + params, err = parseArgs(args[1:]) if err != nil { - c.Err(err) - return + return err } if runCurrent { - md := m.ABI.GetMethod(c.Args[0], len(params)) + md := m.ABI.GetMethod(args[0], len(params)) if md == nil { - c.Err(fmt.Errorf("%w: method not found", ErrInvalidParameter)) - return + return fmt.Errorf("%w: method not found", ErrInvalidParameter) } offset = md.Offset } @@ -457,8 +543,7 @@ func handleRun(c *ishell.Context) { } if runCurrent { if !v.Ready() { - c.Err(fmt.Errorf("no program loaded")) - return + return errors.New("no program loaded") } v.Context().Jump(offset) if initMD := m.ABI.GetMethod(manifest.MethodInit, 0); initMD != nil { @@ -466,15 +551,17 @@ func handleRun(c *ishell.Context) { } } } - runVMWithHandling(c, v) - changePrompt(c, v) + runVMWithHandling(c) + changePrompt(c.App) + return nil } // runVMWithHandling runs VM with handling errors and additional state messages. -func runVMWithHandling(c *ishell.Context, v *vm.VM) { +func runVMWithHandling(c *cli.Context) { + v := getVMFromContext(c.App) err := v.Run() if err != nil { - c.Err(err) + writeErr(c.App.ErrWriter, err) } var message string @@ -493,58 +580,59 @@ func runVMWithHandling(c *ishell.Context, v *vm.VM) { } } if message != "" { - c.Println(message) + fmt.Fprintln(c.App.Writer, message) } } -func handleCont(c *ishell.Context) { - if !checkVMIsReady(c) { - return +func handleCont(c *cli.Context) error { + if !checkVMIsReady(c.App) { + return nil } - v := getVMFromContext(c) - runVMWithHandling(c, v) - changePrompt(c, v) + runVMWithHandling(c) + changePrompt(c.App) + return nil } -func handleStep(c *ishell.Context) { +func handleStep(c *cli.Context) error { var ( n = 1 err error ) - if !checkVMIsReady(c) { - return + if !checkVMIsReady(c.App) { + return nil } - v := getVMFromContext(c) - if len(c.Args) > 0 { - n, err = strconv.Atoi(c.Args[0]) + v := getVMFromContext(c.App) + args := c.Args() + if len(args) > 0 { + n, err = strconv.Atoi(args[0]) if err != nil { - c.Err(fmt.Errorf("%w: %v", ErrInvalidParameter, err)) - return + return fmt.Errorf("%w: %s", ErrInvalidParameter, err) } } v.AddBreakPointRel(n) - runVMWithHandling(c, v) - changePrompt(c, v) + runVMWithHandling(c) + changePrompt(c.App) + return nil } -func handleStepInto(c *ishell.Context) { - handleStepType(c, "into") +func handleStepInto(c *cli.Context) error { + return handleStepType(c, "into") } -func handleStepOut(c *ishell.Context) { - handleStepType(c, "out") +func handleStepOut(c *cli.Context) error { + return handleStepType(c, "out") } -func handleStepOver(c *ishell.Context) { - handleStepType(c, "over") +func handleStepOver(c *cli.Context) error { + return handleStepType(c, "over") } -func handleStepType(c *ishell.Context, stepType string) { - if !checkVMIsReady(c) { - return +func handleStepType(c *cli.Context, stepType string) error { + if !checkVMIsReady(c.App) { + return nil } - v := getVMFromContext(c) + v := getVMFromContext(c.App) var err error switch stepType { case "into": @@ -555,47 +643,69 @@ func handleStepType(c *ishell.Context, stepType string) { err = v.StepOver() } if err != nil { - c.Err(err) - } else { - handleIP(c) + return err } - changePrompt(c, v) + _ = handleIP(c) + changePrompt(c.App) + return nil } -func handleOps(c *ishell.Context) { - if !checkVMIsReady(c) { - return +func handleOps(c *cli.Context) error { + if !checkVMIsReady(c.App) { + return nil } - v := getVMFromContext(c) + v := getVMFromContext(c.App) out := bytes.NewBuffer(nil) v.PrintOps(out) - c.Println(out.String()) + fmt.Fprintln(c.App.Writer, out.String()) + return nil } -func changePrompt(c ishell.Actions, v *vm.VM) { +func changePrompt(app *cli.App) { + v := getVMFromContext(app) + l := getReadlineInstanceFromContext(app) if v.Ready() && v.Context().NextIP() >= 0 && v.Context().NextIP() < v.Context().LenInstr() { - c.SetPrompt(fmt.Sprintf("NEO-GO-VM %d > ", v.Context().NextIP())) + l.SetPrompt(fmt.Sprintf("\033[32mNEO-GO-VM %d >\033[0m ", v.Context().NextIP())) } else { - c.SetPrompt("NEO-GO-VM > ") + l.SetPrompt("\033[32mNEO-GO-VM >\033[0m ") } } // Run waits for user input from Stdin and executes the passed command. func (c *VMCLI) Run() error { - if c.printLogo { - printLogo(c.shell) + if getPrintLogoFromContext(c.shell) { + printLogo(c.shell.Writer) + } + l := getReadlineInstanceFromContext(c.shell) + for { + line, err := l.Readline() + if err == io.EOF || err == readline.ErrInterrupt { + return nil // OK, stop execution. + } + if err != nil { + return fmt.Errorf("failed to read input: %w", err) // Critical error, stop execution. + } + + args, err := shellquote.Split(line) + if err != nil { + writeErr(c.shell.ErrWriter, fmt.Errorf("failed to parse arguments: %w", err)) + continue // Not a critical error, continue execution. + } + + err = c.shell.Run(append([]string{"vm"}, args...)) + if err != nil { + writeErr(c.shell.ErrWriter, err) // Various command/flags parsing errors and execution errors. + } } - c.shell.Run() - return nil } -func handleParse(c *ishell.Context) { - res, err := Parse(c.Args) +func handleParse(c *cli.Context) error { + res, err := Parse(c.Args()) if err != nil { - c.Err(err) - return + return err } - c.Print(res) + fmt.Fprintln(c.App.Writer, res) + return nil } // Parse converts it's argument to other formats. @@ -708,8 +818,13 @@ const logo = ` /_/ |_/_____/\____/ \____/\____/ |___/_/ /_/ ` -func printLogo(c *ishell.Shell) { - c.Print(logo) - c.Println() - c.Println() +func printLogo(w io.Writer) { + fmt.Fprint(w, logo) + fmt.Fprintln(w) + fmt.Fprintln(w) + fmt.Fprintln(w) +} + +func writeErr(w io.Writer, err error) { + fmt.Fprintf(w, "Error: %s\n", err) } diff --git a/pkg/vm/cli/cli_test.go b/pkg/vm/cli/cli_test.go index 8c71a30fd..584e46859 100644 --- a/pkg/vm/cli/cli_test.go +++ b/pkg/vm/cli/cli_test.go @@ -15,7 +15,7 @@ import ( "testing" "time" - "github.com/abiosoft/readline" + "github.com/chzyer/readline" "github.com/nspcc-dev/neo-go/internal/random" "github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/config" @@ -75,6 +75,7 @@ func newTestVMCLIWithLogo(t *testing.T, printLogo bool) *executor { &readline.Config{ Prompt: "", Stdin: e.in, + Stderr: e.out, Stdout: e.out, FuncIsTerminal: func() bool { return false @@ -205,29 +206,35 @@ func TestLoad(t *testing.T) { }` tmpDir := t.TempDir() - t.Run("loadgo", func(t *testing.T) { - filename := filepath.Join(tmpDir, "vmtestcontract.go") - require.NoError(t, ioutil.WriteFile(filename, []byte(src), os.ModePerm)) - filename = "'" + filename + "'" - filenameErr := filepath.Join(tmpDir, "vmtestcontract_err.go") - require.NoError(t, ioutil.WriteFile(filenameErr, []byte(src+"invalid_token"), os.ModePerm)) - filenameErr = "'" + filenameErr + "'" - goMod := []byte(`module test.example/vmcli + checkLoadgo := func(t *testing.T, tName, cName, cErrName string) { + t.Run("loadgo "+tName, func(t *testing.T) { + filename := filepath.Join(tmpDir, cName) + require.NoError(t, ioutil.WriteFile(filename, []byte(src), os.ModePerm)) + filename = "'" + filename + "'" + filenameErr := filepath.Join(tmpDir, cErrName) + require.NoError(t, ioutil.WriteFile(filenameErr, []byte(src+"invalid_token"), os.ModePerm)) + filenameErr = "'" + filenameErr + "'" + goMod := []byte(`module test.example/vmcli go 1.16`) - require.NoError(t, ioutil.WriteFile(filepath.Join(tmpDir, "go.mod"), goMod, os.ModePerm)) + require.NoError(t, ioutil.WriteFile(filepath.Join(tmpDir, "go.mod"), goMod, os.ModePerm)) - e := newTestVMCLI(t) - e.runProgWithTimeout(t, 10*time.Second, - "loadgo", - "loadgo "+filenameErr, - "loadgo "+filename, - "run main add 3 5") + e := newTestVMCLI(t) + e.runProgWithTimeout(t, 10*time.Second, + "loadgo", + "loadgo "+filenameErr, + "loadgo "+filename, + "run main add 3 5") + + e.checkError(t, ErrMissingParameter) + e.checkNextLine(t, "Error:") + e.checkNextLine(t, "READY: loaded \\d* instructions") + e.checkStack(t, 8) + }) + } + + checkLoadgo(t, "simple", "vmtestcontract.go", "vmtestcontract_err.go") + checkLoadgo(t, "utf-8 with spaces", "тестовый контракт.go", "тестовый контракт с ошибкой.go") - e.checkError(t, ErrMissingParameter) - e.checkNextLine(t, "Error:") - e.checkNextLine(t, "READY: loaded \\d* instructions") - e.checkStack(t, 8) - }) t.Run("loadgo, check calling flags", func(t *testing.T) { srcAllowNotify := `package kek import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" @@ -640,3 +647,19 @@ func TestExit(t *testing.T) { e.runProg(t, "exit") require.True(t, e.exit.Load()) } + +func TestReset(t *testing.T) { + script := []byte{byte(opcode.PUSH1)} + e := newTestVMCLI(t) + e.runProg(t, + "loadhex "+hex.EncodeToString(script), + "ops", + "reset", + "ops") + + e.checkNextLine(t, "READY: loaded 1 instructions") + e.checkNextLine(t, "INDEX.*OPCODE.*PARAMETER") + e.checkNextLine(t, "0.*PUSH1.*") + e.checkNextLine(t, "") + e.checkError(t, fmt.Errorf("VM is not ready: no program loaded")) +}