forked from TrueCloudLab/neoneo-go
cli: support escape codes
This commit is contained in:
parent
2ee755e09f
commit
f6176b30f2
9 changed files with 94 additions and 47 deletions
|
@ -1,7 +1,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
@ -27,7 +26,7 @@ import (
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"go.uber.org/zap/zaptest"
|
"go.uber.org/zap/zaptest"
|
||||||
"golang.org/x/crypto/ssh/terminal"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -100,9 +99,6 @@ func newExecutorWithConfig(t *testing.T, needChain bool, f func(*config.Config))
|
||||||
}
|
}
|
||||||
e.CLI.Writer = e.Out
|
e.CLI.Writer = e.Out
|
||||||
e.CLI.ErrWriter = e.Err
|
e.CLI.ErrWriter = e.Err
|
||||||
rw := bufio.NewReadWriter(bufio.NewReader(e.In), bufio.NewWriter(ioutil.Discard))
|
|
||||||
require.Nil(t, input.Terminal) // check that tests clean up properly
|
|
||||||
input.Terminal = terminal.NewTerminal(rw, "")
|
|
||||||
if needChain {
|
if needChain {
|
||||||
e.Chain, e.RPC, e.NetSrv = newTestChain(t, f)
|
e.Chain, e.RPC, e.NetSrv = newTestChain(t, f)
|
||||||
}
|
}
|
||||||
|
@ -191,7 +187,12 @@ func (e *executor) Run(t *testing.T, args ...string) {
|
||||||
func (e *executor) run(args ...string) error {
|
func (e *executor) run(args ...string) error {
|
||||||
e.Out.Reset()
|
e.Out.Reset()
|
||||||
e.Err.Reset()
|
e.Err.Reset()
|
||||||
|
input.Terminal = term.NewTerminal(input.ReadWriter{
|
||||||
|
Reader: e.In,
|
||||||
|
Writer: ioutil.Discard,
|
||||||
|
}, "")
|
||||||
err := e.CLI.Run(args)
|
err := e.CLI.Run(args)
|
||||||
|
input.Terminal = nil
|
||||||
e.In.Reset()
|
e.In.Reset()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,44 +1,57 @@
|
||||||
package input
|
package input
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh/terminal"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Terminal is a terminal used for input. If `nil`, stdin is used.
|
// Terminal is a terminal used for input. If `nil`, stdin is used.
|
||||||
var Terminal *terminal.Terminal
|
var Terminal *term.Terminal
|
||||||
|
|
||||||
// ReadLine reads line from the input without trailing '\n'
|
// ReadWriter combiner reader and writer.
|
||||||
func ReadLine(w io.Writer, prompt string) (string, error) {
|
type ReadWriter struct {
|
||||||
if Terminal != nil {
|
io.Reader
|
||||||
_, err := Terminal.Write([]byte(prompt))
|
io.Writer
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
raw, err := Terminal.ReadLine()
|
|
||||||
return strings.TrimRight(raw, "\n"), err
|
|
||||||
}
|
|
||||||
fmt.Fprint(w, prompt)
|
|
||||||
buf := bufio.NewReader(os.Stdin)
|
|
||||||
return buf.ReadString('\n')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadPassword reads user password with prompt.
|
// ReadLine reads line from the input without trailing '\n'
|
||||||
func ReadPassword(w io.Writer, prompt string) (string, error) {
|
func ReadLine(prompt string) (string, error) {
|
||||||
if Terminal != nil {
|
trm := Terminal
|
||||||
return Terminal.ReadPassword(prompt)
|
if trm == nil {
|
||||||
|
s, err := term.MakeRaw(syscall.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer term.Restore(syscall.Stdin, s)
|
||||||
|
trm = term.NewTerminal(ReadWriter{
|
||||||
|
Reader: os.Stdin,
|
||||||
|
Writer: os.Stdout,
|
||||||
|
}, "")
|
||||||
}
|
}
|
||||||
fmt.Fprint(w, prompt)
|
return readLine(trm, prompt)
|
||||||
rawPass, err := terminal.ReadPassword(syscall.Stdin)
|
}
|
||||||
|
|
||||||
|
func readLine(trm *term.Terminal, prompt string) (string, error) {
|
||||||
|
_, err := trm.Write([]byte(prompt))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
fmt.Fprintln(w)
|
return trm.ReadLine()
|
||||||
return strings.TrimRight(string(rawPass), "\n"), nil
|
}
|
||||||
|
|
||||||
|
// ReadPassword reads user password with prompt.
|
||||||
|
func ReadPassword(prompt string) (string, error) {
|
||||||
|
trm := Terminal
|
||||||
|
if trm == nil {
|
||||||
|
s, err := term.MakeRaw(syscall.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer term.Restore(syscall.Stdin, s)
|
||||||
|
trm = term.NewTerminal(ReadWriter{os.Stdin, os.Stdout}, prompt)
|
||||||
|
}
|
||||||
|
return trm.ReadPassword(prompt)
|
||||||
}
|
}
|
||||||
|
|
|
@ -770,7 +770,7 @@ func getAccFromContext(ctx *cli.Context) (*wallet.Account, error) {
|
||||||
return nil, cli.NewExitError(fmt.Errorf("wallet contains no account for '%s'", address.Uint160ToString(addr)), 1)
|
return nil, cli.NewExitError(fmt.Errorf("wallet contains no account for '%s'", address.Uint160ToString(addr)), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
rawPass, err := input.ReadPassword(ctx.App.Writer,
|
rawPass, err := input.ReadPassword(
|
||||||
fmt.Sprintf("Enter account %s password > ", address.Uint160ToString(addr)))
|
fmt.Sprintf("Enter account %s password > ", address.Uint160ToString(addr)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, cli.NewExitError(err, 1)
|
return nil, cli.NewExitError(err, 1)
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package vm
|
package vm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/abiosoft/readline"
|
||||||
vmcli "github.com/nspcc-dev/neo-go/pkg/vm/cli"
|
vmcli "github.com/nspcc-dev/neo-go/pkg/vm/cli"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
@ -18,6 +21,9 @@ func NewCommands() []cli.Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
func startVMPrompt(ctx *cli.Context) error {
|
func startVMPrompt(ctx *cli.Context) error {
|
||||||
p := vmcli.New()
|
p := vmcli.NewWithConfig(true, os.Exit, &readline.Config{
|
||||||
|
Stdout: ctx.App.Writer,
|
||||||
|
Stderr: ctx.App.ErrWriter,
|
||||||
|
})
|
||||||
return p.Run()
|
return p.Run()
|
||||||
}
|
}
|
||||||
|
|
|
@ -197,7 +197,7 @@ func getDecryptedAccount(ctx *cli.Context, wall *wallet.Wallet, addr util.Uint16
|
||||||
return nil, fmt.Errorf("can't find account for the address: %s", address.Uint160ToString(addr))
|
return nil, fmt.Errorf("can't find account for the address: %s", address.Uint160ToString(addr))
|
||||||
}
|
}
|
||||||
|
|
||||||
if pass, err := input.ReadPassword(ctx.App.Writer, "Password > "); err != nil {
|
if pass, err := input.ReadPassword("Password > "); err != nil {
|
||||||
fmt.Println("ERROR", pass, err)
|
fmt.Println("ERROR", pass, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if err := acc.Decrypt(pass); err != nil {
|
} else if err := acc.Decrypt(pass); err != nil {
|
||||||
|
|
|
@ -261,7 +261,7 @@ func convertWallet(ctx *cli.Context) error {
|
||||||
defer newWallet.Close()
|
defer newWallet.Close()
|
||||||
|
|
||||||
for _, acc := range wall.Accounts {
|
for _, acc := range wall.Accounts {
|
||||||
pass, err := input.ReadPassword(ctx.App.Writer, fmt.Sprintf("Enter passphrase for account %s (label '%s') > ", acc.Address, acc.Label))
|
pass, err := input.ReadPassword(fmt.Sprintf("Enter passphrase for account %s (label '%s') > ", acc.Address, acc.Label))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cli.NewExitError(err, 1)
|
return cli.NewExitError(err, 1)
|
||||||
}
|
}
|
||||||
|
@ -331,7 +331,7 @@ loop:
|
||||||
|
|
||||||
for _, wif := range wifs {
|
for _, wif := range wifs {
|
||||||
if decrypt {
|
if decrypt {
|
||||||
pass, err := input.ReadPassword(ctx.App.Writer, "Enter password > ")
|
pass, err := input.ReadPassword("Enter password > ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cli.NewExitError(err, 1)
|
return cli.NewExitError(err, 1)
|
||||||
}
|
}
|
||||||
|
@ -505,7 +505,7 @@ func removeAccount(ctx *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func askForConsent(w io.Writer) bool {
|
func askForConsent(w io.Writer) bool {
|
||||||
response, err := input.ReadLine(w, "Are you sure? [y/N]: ")
|
response, err := input.ReadLine("Are you sure? [y/N]: ")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
response = strings.ToLower(strings.TrimSpace(response))
|
response = strings.ToLower(strings.TrimSpace(response))
|
||||||
if response == "y" || response == "yes" {
|
if response == "y" || response == "yes" {
|
||||||
|
@ -522,7 +522,7 @@ func dumpWallet(ctx *cli.Context) error {
|
||||||
return cli.NewExitError(err, 1)
|
return cli.NewExitError(err, 1)
|
||||||
}
|
}
|
||||||
if ctx.Bool("decrypt") {
|
if ctx.Bool("decrypt") {
|
||||||
pass, err := input.ReadPassword(ctx.App.Writer, "Enter wallet password > ")
|
pass, err := input.ReadPassword("Enter wallet password > ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cli.NewExitError(err, 1)
|
return cli.NewExitError(err, 1)
|
||||||
}
|
}
|
||||||
|
@ -563,12 +563,12 @@ func createWallet(ctx *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func readAccountInfo(w io.Writer) (string, string, error) {
|
func readAccountInfo(w io.Writer) (string, string, error) {
|
||||||
rawName, _ := input.ReadLine(w, "Enter the name of the account > ")
|
rawName, _ := input.ReadLine("Enter the name of the account > ")
|
||||||
phrase, err := input.ReadPassword(w, "Enter passphrase > ")
|
phrase, err := input.ReadPassword("Enter passphrase > ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
phraseCheck, err := input.ReadPassword(w, "Confirm passphrase > ")
|
phraseCheck, err := input.ReadPassword("Confirm passphrase > ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
@ -600,7 +600,7 @@ func newAccountFromWIF(w io.Writer, wif string) (*wallet.Account, error) {
|
||||||
// note: NEP2 strings always have length of 58 even though
|
// note: NEP2 strings always have length of 58 even though
|
||||||
// base58 strings can have different lengths even if slice lengths are equal
|
// base58 strings can have different lengths even if slice lengths are equal
|
||||||
if len(wif) == 58 {
|
if len(wif) == 58 {
|
||||||
pass, err := input.ReadPassword(w, "Enter password > ")
|
pass, err := input.ReadPassword("Enter password > ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/abiosoft/readline"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
"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/crypto/keys"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
||||||
|
@ -19,13 +20,37 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestWalletInit(t *testing.T) {
|
func TestWalletInit(t *testing.T) {
|
||||||
tmpDir := os.TempDir()
|
tmpDir := path.Join(os.TempDir(), "neogo.test.walletinit")
|
||||||
|
require.NoError(t, os.Mkdir(tmpDir, os.ModePerm))
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
e := newExecutor(t, false)
|
e := newExecutor(t, false)
|
||||||
defer e.Close(t)
|
defer e.Close(t)
|
||||||
|
|
||||||
walletPath := path.Join(tmpDir, "wallet.json")
|
walletPath := path.Join(tmpDir, "wallet.json")
|
||||||
e.Run(t, "neo-go", "wallet", "init", "--wallet", walletPath)
|
e.Run(t, "neo-go", "wallet", "init", "--wallet", walletPath)
|
||||||
defer os.Remove(walletPath)
|
|
||||||
|
t.Run("terminal escape codes", func(t *testing.T) {
|
||||||
|
walletPath := path.Join(tmpDir, "walletrussian.json")
|
||||||
|
bksp := string([]byte{
|
||||||
|
byte(readline.CharBackward),
|
||||||
|
byte(readline.CharDelete),
|
||||||
|
})
|
||||||
|
e.In.WriteString("буквыы" +
|
||||||
|
bksp + bksp + bksp +
|
||||||
|
"andmore\r")
|
||||||
|
e.In.WriteString("пароу" + bksp + "ль\r")
|
||||||
|
e.In.WriteString("пароль\r")
|
||||||
|
e.Run(t, "neo-go", "wallet", "init", "--account",
|
||||||
|
"--wallet", walletPath)
|
||||||
|
|
||||||
|
w, err := wallet.NewWalletFromFile(walletPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, w.Accounts, 1)
|
||||||
|
require.Equal(t, "букandmore", w.Accounts[0].Label)
|
||||||
|
require.NoError(t, w.Accounts[0].Decrypt("пароль"))
|
||||||
|
w.Close()
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("CreateAccount", func(t *testing.T) {
|
t.Run("CreateAccount", func(t *testing.T) {
|
||||||
e.In.WriteString("testname\r")
|
e.In.WriteString("testname\r")
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -23,7 +23,7 @@ require (
|
||||||
go.uber.org/atomic v1.4.0
|
go.uber.org/atomic v1.4.0
|
||||||
go.uber.org/zap v1.10.0
|
go.uber.org/zap v1.10.0
|
||||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
|
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd // indirect
|
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf
|
||||||
golang.org/x/text v0.3.0
|
golang.org/x/text v0.3.0
|
||||||
golang.org/x/tools v0.0.0-20180318012157-96caea41033d
|
golang.org/x/tools v0.0.0-20180318012157-96caea41033d
|
||||||
gopkg.in/abiosoft/ishell.v2 v2.0.0
|
gopkg.in/abiosoft/ishell.v2 v2.0.0
|
||||||
|
|
6
go.sum
6
go.sum
|
@ -297,8 +297,10 @@ golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/nt
|
||||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
|
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
|
||||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
|
||||||
|
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
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/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 h1:Xmo0nLTRYewf0eXDvo12nMSuOgNQ4283hdbOHIUf7h8=
|
||||||
|
|
Loading…
Reference in a new issue