forked from TrueCloudLab/neoneo-go
Merge pull request #1727 from nspcc-dev/nonascii
Handle terminal escape codes correctly in CLI
This commit is contained in:
commit
11c89257b9
9 changed files with 94 additions and 47 deletions
|
@ -1,7 +1,6 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
|
@ -27,7 +26,7 @@ import (
|
|||
"github.com/urfave/cli"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zaptest"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -100,9 +99,6 @@ func newExecutorWithConfig(t *testing.T, needChain bool, f func(*config.Config))
|
|||
}
|
||||
e.CLI.Writer = e.Out
|
||||
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 {
|
||||
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 {
|
||||
e.Out.Reset()
|
||||
e.Err.Reset()
|
||||
input.Terminal = term.NewTerminal(input.ReadWriter{
|
||||
Reader: e.In,
|
||||
Writer: ioutil.Discard,
|
||||
}, "")
|
||||
err := e.CLI.Run(args)
|
||||
input.Terminal = nil
|
||||
e.In.Reset()
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -1,44 +1,57 @@
|
|||
package input
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// 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'
|
||||
func ReadLine(w io.Writer, prompt string) (string, error) {
|
||||
if Terminal != nil {
|
||||
_, err := Terminal.Write([]byte(prompt))
|
||||
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')
|
||||
// ReadWriter combiner reader and writer.
|
||||
type ReadWriter struct {
|
||||
io.Reader
|
||||
io.Writer
|
||||
}
|
||||
|
||||
// ReadPassword reads user password with prompt.
|
||||
func ReadPassword(w io.Writer, prompt string) (string, error) {
|
||||
if Terminal != nil {
|
||||
return Terminal.ReadPassword(prompt)
|
||||
// ReadLine reads line from the input without trailing '\n'
|
||||
func ReadLine(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{
|
||||
Reader: os.Stdin,
|
||||
Writer: os.Stdout,
|
||||
}, "")
|
||||
}
|
||||
fmt.Fprint(w, prompt)
|
||||
rawPass, err := terminal.ReadPassword(syscall.Stdin)
|
||||
return readLine(trm, prompt)
|
||||
}
|
||||
|
||||
func readLine(trm *term.Terminal, prompt string) (string, error) {
|
||||
_, err := trm.Write([]byte(prompt))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
return strings.TrimRight(string(rawPass), "\n"), nil
|
||||
return trm.ReadLine()
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
rawPass, err := input.ReadPassword(ctx.App.Writer,
|
||||
rawPass, err := input.ReadPassword(
|
||||
fmt.Sprintf("Enter account %s password > ", address.Uint160ToString(addr)))
|
||||
if err != nil {
|
||||
return nil, cli.NewExitError(err, 1)
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package vm
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/abiosoft/readline"
|
||||
vmcli "github.com/nspcc-dev/neo-go/pkg/vm/cli"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
@ -18,6 +21,9 @@ func NewCommands() []cli.Command {
|
|||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
if pass, err := input.ReadPassword(ctx.App.Writer, "Password > "); err != nil {
|
||||
if pass, err := input.ReadPassword("Password > "); err != nil {
|
||||
fmt.Println("ERROR", pass, err)
|
||||
return nil, err
|
||||
} else if err := acc.Decrypt(pass); err != nil {
|
||||
|
|
|
@ -261,7 +261,7 @@ func convertWallet(ctx *cli.Context) error {
|
|||
defer newWallet.Close()
|
||||
|
||||
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 {
|
||||
return cli.NewExitError(err, 1)
|
||||
}
|
||||
|
@ -331,7 +331,7 @@ loop:
|
|||
|
||||
for _, wif := range wifs {
|
||||
if decrypt {
|
||||
pass, err := input.ReadPassword(ctx.App.Writer, "Enter password > ")
|
||||
pass, err := input.ReadPassword("Enter password > ")
|
||||
if err != nil {
|
||||
return cli.NewExitError(err, 1)
|
||||
}
|
||||
|
@ -505,7 +505,7 @@ func removeAccount(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
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 {
|
||||
response = strings.ToLower(strings.TrimSpace(response))
|
||||
if response == "y" || response == "yes" {
|
||||
|
@ -522,7 +522,7 @@ func dumpWallet(ctx *cli.Context) error {
|
|||
return cli.NewExitError(err, 1)
|
||||
}
|
||||
if ctx.Bool("decrypt") {
|
||||
pass, err := input.ReadPassword(ctx.App.Writer, "Enter wallet password > ")
|
||||
pass, err := input.ReadPassword("Enter wallet password > ")
|
||||
if err != nil {
|
||||
return cli.NewExitError(err, 1)
|
||||
}
|
||||
|
@ -563,12 +563,12 @@ func createWallet(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
func readAccountInfo(w io.Writer) (string, string, error) {
|
||||
rawName, _ := input.ReadLine(w, "Enter the name of the account > ")
|
||||
phrase, err := input.ReadPassword(w, "Enter passphrase > ")
|
||||
rawName, _ := input.ReadLine("Enter the name of the account > ")
|
||||
phrase, err := input.ReadPassword("Enter passphrase > ")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
phraseCheck, err := input.ReadPassword(w, "Confirm passphrase > ")
|
||||
phraseCheck, err := input.ReadPassword("Confirm passphrase > ")
|
||||
if err != nil {
|
||||
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
|
||||
// base58 strings can have different lengths even if slice lengths are equal
|
||||
if len(wif) == 58 {
|
||||
pass, err := input.ReadPassword(w, "Enter password > ")
|
||||
pass, err := input.ReadPassword("Enter password > ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/abiosoft/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"
|
||||
|
@ -19,13 +20,37 @@ import (
|
|||
)
|
||||
|
||||
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)
|
||||
defer e.Close(t)
|
||||
|
||||
walletPath := path.Join(tmpDir, "wallet.json")
|
||||
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) {
|
||||
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/zap v1.10.0
|
||||
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/tools v0.0.0-20180318012157-96caea41033d
|
||||
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-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
|
||||
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-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
|
||||
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/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20180318012157-96caea41033d h1:Xmo0nLTRYewf0eXDvo12nMSuOgNQ4283hdbOHIUf7h8=
|
||||
|
|
Loading…
Reference in a new issue