From dff9b4246f3ef9e6c254b57eef6d0433809f16b9 Mon Sep 17 00:00:00 2001
From: Felix Lange <fjl@twurst.com>
Date: Mon, 21 Mar 2016 14:05:22 +0100
Subject: cmd/geth, cmd/utils: improve input handling

These changes make prompting behave consistently on all platforms:

* The input buffer is now global.
  Buffering was previously set up for each prompt, which can cause weird
  behaviour, e.g. when running "geth account update <input.txt" where
  input.txt contains three lines. In this case, the first password
  prompt would fill up the buffer with all lines and then use only the
  first one.

* Print the "unsupported terminal" warning only once.
  Now that stdin prompting has global state, we can use it to track
  the warning there.

* Work around small liner issues, particularly on Windows.
  Prompting didn't work under most of the third-party terminal emulators
  on Windows because liner assumes line editing is always available.
---
 cmd/geth/chaincmd.go |  2 +-
 cmd/geth/js.go       | 83 +++++++++++---------------------------------
 cmd/geth/js_test.go  |  2 +-
 cmd/geth/main.go     |  5 +--
 cmd/utils/cmd.go     | 50 ---------------------------
 cmd/utils/input.go   | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 cmd/utils/jeth.go    |  9 ++---
 7 files changed, 128 insertions(+), 121 deletions(-)
 create mode 100644 cmd/utils/input.go

(limited to 'cmd')

diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go
index 868ee7db1..32eacc99e 100644
--- a/cmd/geth/chaincmd.go
+++ b/cmd/geth/chaincmd.go
@@ -116,7 +116,7 @@ func exportChain(ctx *cli.Context) {
 }
 
 func removeDB(ctx *cli.Context) {
-	confirm, err := utils.PromptConfirm("Remove local database?")
+	confirm, err := utils.Stdin.ConfirmPrompt("Remove local database?")
 	if err != nil {
 		utils.Fatalf("%v", err)
 	}
diff --git a/cmd/geth/js.go b/cmd/geth/js.go
index 68c906443..5178465d1 100644
--- a/cmd/geth/js.go
+++ b/cmd/geth/js.go
@@ -17,7 +17,6 @@
 package main
 
 import (
-	"bufio"
 	"fmt"
 	"math/big"
 	"os"
@@ -46,30 +45,6 @@ var (
 	exit           = regexp.MustCompile("^\\s*exit\\s*;*\\s*$")
 )
 
-type prompter interface {
-	AppendHistory(string)
-	Prompt(p string) (string, error)
-	PasswordPrompt(p string) (string, error)
-}
-
-type dumbterm struct{ r *bufio.Reader }
-
-func (r dumbterm) Prompt(p string) (string, error) {
-	fmt.Print(p)
-	line, err := r.r.ReadString('\n')
-	return strings.TrimSuffix(line, "\n"), err
-}
-
-func (r dumbterm) PasswordPrompt(p string) (string, error) {
-	fmt.Println("!! Unsupported terminal, password will echo.")
-	fmt.Print(p)
-	input, err := bufio.NewReader(os.Stdin).ReadString('\n')
-	fmt.Println()
-	return input, err
-}
-
-func (r dumbterm) AppendHistory(string) {}
-
 type jsre struct {
 	re         *re.JSRE
 	stack      *node.Node
@@ -78,7 +53,6 @@ type jsre struct {
 	atexit     func()
 	corsDomain string
 	client     rpc.Client
-	prompter
 }
 
 func makeCompleter(re *jsre) liner.WordCompleter {
@@ -106,27 +80,11 @@ func newLightweightJSRE(docRoot string, client rpc.Client, datadir string, inter
 	js := &jsre{ps1: "> "}
 	js.wait = make(chan *big.Int)
 	js.client = client
-
 	js.re = re.New(docRoot)
 	if err := js.apiBindings(); err != nil {
 		utils.Fatalf("Unable to initialize console - %v", err)
 	}
-
-	if !liner.TerminalSupported() || !interactive {
-		js.prompter = dumbterm{bufio.NewReader(os.Stdin)}
-	} else {
-		lr := liner.NewLiner()
-		js.withHistory(datadir, func(hist *os.File) { lr.ReadHistory(hist) })
-		lr.SetCtrlCAborts(true)
-		lr.SetWordCompleter(makeCompleter(js))
-		lr.SetTabCompletionStyle(liner.TabPrints)
-		js.prompter = lr
-		js.atexit = func() {
-			js.withHistory(datadir, func(hist *os.File) { hist.Truncate(0); lr.WriteHistory(hist) })
-			lr.Close()
-			close(js.wait)
-		}
-	}
+	js.setupInput(datadir)
 	return js
 }
 
@@ -136,28 +94,27 @@ func newJSRE(stack *node.Node, docRoot, corsDomain string, client rpc.Client, in
 	js.corsDomain = corsDomain
 	js.wait = make(chan *big.Int)
 	js.client = client
-
 	js.re = re.New(docRoot)
 	if err := js.apiBindings(); err != nil {
 		utils.Fatalf("Unable to connect - %v", err)
 	}
+	js.setupInput(stack.DataDir())
+	return js
+}
 
-	if !liner.TerminalSupported() || !interactive {
-		js.prompter = dumbterm{bufio.NewReader(os.Stdin)}
-	} else {
-		lr := liner.NewLiner()
-		js.withHistory(stack.DataDir(), func(hist *os.File) { lr.ReadHistory(hist) })
-		lr.SetCtrlCAborts(true)
-		lr.SetWordCompleter(makeCompleter(js))
-		lr.SetTabCompletionStyle(liner.TabPrints)
-		js.prompter = lr
-		js.atexit = func() {
-			js.withHistory(stack.DataDir(), func(hist *os.File) { hist.Truncate(0); lr.WriteHistory(hist) })
-			lr.Close()
-			close(js.wait)
-		}
+func (self *jsre) setupInput(datadir string) {
+	self.withHistory(datadir, func(hist *os.File) { utils.Stdin.ReadHistory(hist) })
+	utils.Stdin.SetCtrlCAborts(true)
+	utils.Stdin.SetWordCompleter(makeCompleter(self))
+	utils.Stdin.SetTabCompletionStyle(liner.TabPrints)
+	self.atexit = func() {
+		self.withHistory(datadir, func(hist *os.File) {
+			hist.Truncate(0)
+			utils.Stdin.WriteHistory(hist)
+		})
+		utils.Stdin.Close()
+		close(self.wait)
 	}
-	return js
 }
 
 func (self *jsre) batch(statement string) {
@@ -290,7 +247,7 @@ func (js *jsre) apiBindings() error {
 }
 
 func (self *jsre) AskPassword() (string, bool) {
-	pass, err := self.PasswordPrompt("Passphrase: ")
+	pass, err := utils.Stdin.PasswordPrompt("Passphrase: ")
 	if err != nil {
 		return "", false
 	}
@@ -315,7 +272,7 @@ func (self *jsre) ConfirmTransaction(tx string) bool {
 
 func (self *jsre) UnlockAccount(addr []byte) bool {
 	fmt.Printf("Please unlock account %x.\n", addr)
-	pass, err := self.PasswordPrompt("Passphrase: ")
+	pass, err := utils.Stdin.PasswordPrompt("Passphrase: ")
 	if err != nil {
 		return false
 	}
@@ -365,7 +322,7 @@ func (self *jsre) interactive() {
 	go func() {
 		defer close(inputln)
 		for {
-			line, err := self.Prompt(<-prompt)
+			line, err := utils.Stdin.Prompt(<-prompt)
 			if err != nil {
 				if err == liner.ErrPromptAborted { // ctrl-C
 					self.resetPrompt()
@@ -404,7 +361,7 @@ func (self *jsre) interactive() {
 			self.setIndent()
 			if indentCount <= 0 {
 				if mustLogInHistory(str) {
-					self.AppendHistory(str[:len(str)-1])
+					utils.Stdin.AppendHistory(str[:len(str)-1])
 				}
 				self.parseInput(str)
 				str = ""
diff --git a/cmd/geth/js_test.go b/cmd/geth/js_test.go
index e0c4dacbc..efdb9ab86 100644
--- a/cmd/geth/js_test.go
+++ b/cmd/geth/js_test.go
@@ -94,7 +94,7 @@ func testREPL(t *testing.T, config func(*eth.Config)) (string, *testjethre, *nod
 		t.Fatal(err)
 	}
 	// Create a networkless protocol stack
-	stack, err := node.New(&node.Config{PrivateKey: testNodeKey, Name: "test", NoDiscovery: true})
+	stack, err := node.New(&node.Config{DataDir: tmp, PrivateKey: testNodeKey, Name: "test", NoDiscovery: true})
 	if err != nil {
 		t.Fatalf("failed to create node: %v", err)
 	}
diff --git a/cmd/geth/main.go b/cmd/geth/main.go
index 584df7316..c83735e30 100644
--- a/cmd/geth/main.go
+++ b/cmd/geth/main.go
@@ -373,6 +373,7 @@ JavaScript API. See https://github.com/ethereum/go-ethereum/wiki/Javascipt-Conso
 	app.After = func(ctx *cli.Context) error {
 		logger.Flush()
 		debug.Exit()
+		utils.Stdin.Close() // Resets terminal mode.
 		return nil
 	}
 }
@@ -595,12 +596,12 @@ func getPassPhrase(prompt string, confirmation bool, i int, passwords []string)
 	}
 	// Otherwise prompt the user for the password
 	fmt.Println(prompt)
-	password, err := utils.PromptPassword("Passphrase: ", true)
+	password, err := utils.Stdin.PasswordPrompt("Passphrase: ")
 	if err != nil {
 		utils.Fatalf("Failed to read passphrase: %v", err)
 	}
 	if confirmation {
-		confirm, err := utils.PromptPassword("Repeat passphrase: ", false)
+		confirm, err := utils.Stdin.PasswordPrompt("Repeat passphrase: ")
 		if err != nil {
 			utils.Fatalf("Failed to read passphrase confirmation: %v", err)
 		}
diff --git a/cmd/utils/cmd.go b/cmd/utils/cmd.go
index eb7907b0c..d331f762f 100644
--- a/cmd/utils/cmd.go
+++ b/cmd/utils/cmd.go
@@ -18,13 +18,11 @@
 package utils
 
 import (
-	"bufio"
 	"fmt"
 	"io"
 	"os"
 	"os/signal"
 	"regexp"
-	"strings"
 
 	"github.com/ethereum/go-ethereum/common"
 	"github.com/ethereum/go-ethereum/core"
@@ -34,17 +32,12 @@ import (
 	"github.com/ethereum/go-ethereum/logger/glog"
 	"github.com/ethereum/go-ethereum/node"
 	"github.com/ethereum/go-ethereum/rlp"
-	"github.com/peterh/liner"
 )
 
 const (
 	importBatchSize = 2500
 )
 
-var (
-	interruptCallbacks = []func(os.Signal){}
-)
-
 func openLogFile(Datadir string, filename string) *os.File {
 	path := common.AbsolutePath(Datadir, filename)
 	file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
@@ -54,49 +47,6 @@ func openLogFile(Datadir string, filename string) *os.File {
 	return file
 }
 
-func PromptConfirm(prompt string) (bool, error) {
-	var (
-		input string
-		err   error
-	)
-	prompt = prompt + " [y/N] "
-
-	// if liner.TerminalSupported() {
-	// 	fmt.Println("term")
-	// 	lr := liner.NewLiner()
-	// 	defer lr.Close()
-	// 	input, err = lr.Prompt(prompt)
-	// } else {
-	fmt.Print(prompt)
-	input, err = bufio.NewReader(os.Stdin).ReadString('\n')
-	fmt.Println()
-	// }
-
-	if len(input) > 0 && strings.ToUpper(input[:1]) == "Y" {
-		return true, nil
-	} else {
-		return false, nil
-	}
-
-	return false, err
-}
-
-func PromptPassword(prompt string, warnTerm bool) (string, error) {
-	if liner.TerminalSupported() {
-		lr := liner.NewLiner()
-		defer lr.Close()
-		return lr.PasswordPrompt(prompt)
-	}
-	if warnTerm {
-		fmt.Println("!! Unsupported terminal, password will be echoed.")
-	}
-	fmt.Print(prompt)
-	input, err := bufio.NewReader(os.Stdin).ReadString('\n')
-	input = strings.TrimRight(input, "\r\n")
-	fmt.Println()
-	return input, err
-}
-
 // Fatalf formats a message to standard error and exits the program.
 // The message is also printed to standard output if standard error
 // is redirected to a different file.
diff --git a/cmd/utils/input.go b/cmd/utils/input.go
new file mode 100644
index 000000000..523d5a587
--- /dev/null
+++ b/cmd/utils/input.go
@@ -0,0 +1,98 @@
+// Copyright 2016 The go-ethereum Authors
+// This file is part of go-ethereum.
+//
+// go-ethereum is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// go-ethereum is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
+
+package utils
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/peterh/liner"
+)
+
+// Holds the stdin line reader.
+// Only this reader may be used for input because it keeps
+// an internal buffer.
+var Stdin = newUserInputReader()
+
+type userInputReader struct {
+	*liner.State
+	warned     bool
+	supported  bool
+	normalMode liner.ModeApplier
+	rawMode    liner.ModeApplier
+}
+
+func newUserInputReader() *userInputReader {
+	r := new(userInputReader)
+	// Get the original mode before calling NewLiner.
+	// This is usually regular "cooked" mode where characters echo.
+	normalMode, _ := liner.TerminalMode()
+	// Turn on liner. It switches to raw mode.
+	r.State = liner.NewLiner()
+	rawMode, err := liner.TerminalMode()
+	if err != nil || !liner.TerminalSupported() {
+		r.supported = false
+	} else {
+		r.supported = true
+		r.normalMode = normalMode
+		r.rawMode = rawMode
+		// Switch back to normal mode while we're not prompting.
+		normalMode.ApplyMode()
+	}
+	return r
+}
+
+func (r *userInputReader) Prompt(prompt string) (string, error) {
+	if r.supported {
+		r.rawMode.ApplyMode()
+		defer r.normalMode.ApplyMode()
+	} else {
+		// liner tries to be smart about printing the prompt
+		// and doesn't print anything if input is redirected.
+		// Un-smart it by printing the prompt always.
+		fmt.Print(prompt)
+		prompt = ""
+		defer fmt.Println()
+	}
+	return r.State.Prompt(prompt)
+}
+
+func (r *userInputReader) PasswordPrompt(prompt string) (passwd string, err error) {
+	if r.supported {
+		r.rawMode.ApplyMode()
+		defer r.normalMode.ApplyMode()
+		return r.State.PasswordPrompt(prompt)
+	}
+	if !r.warned {
+		fmt.Println("!! Unsupported terminal, password will be echoed.")
+		r.warned = true
+	}
+	// Just as in Prompt, handle printing the prompt here instead of relying on liner.
+	fmt.Print(prompt)
+	passwd, err = r.State.Prompt("")
+	fmt.Println()
+	return passwd, err
+}
+
+func (r *userInputReader) ConfirmPrompt(prompt string) (bool, error) {
+	prompt = prompt + " [y/N] "
+	input, err := r.Prompt(prompt)
+	if len(input) > 0 && strings.ToUpper(input[:1]) == "Y" {
+		return true, nil
+	}
+	return false, err
+}
diff --git a/cmd/utils/jeth.go b/cmd/utils/jeth.go
index 35fcd4bed..708d457c6 100644
--- a/cmd/utils/jeth.go
+++ b/cmd/utils/jeth.go
@@ -75,8 +75,9 @@ func (self *Jeth) UnlockAccount(call otto.FunctionCall) (response otto.Value) {
 	// if password is not given or as null value -> ask user for password
 	if call.Argument(1).IsUndefined() || call.Argument(1).IsNull() {
 		fmt.Printf("Unlock account %s\n", account)
-		if password, err := PromptPassword("Passphrase: ", true); err == nil {
-			passwd, _ = otto.ToValue(password)
+		if input, err := Stdin.PasswordPrompt("Passphrase: "); err != nil {
+			return otto.FalseValue()
+			passwd, _ = otto.ToValue(input)
 		} else {
 			throwJSExeception(err.Error())
 		}
@@ -111,11 +112,11 @@ func (self *Jeth) NewAccount(call otto.FunctionCall) (response otto.Value) {
 	var passwd string
 	if len(call.ArgumentList) == 0 {
 		var err error
-		passwd, err = PromptPassword("Passphrase: ", true)
+		passwd, err = Stdin.PasswordPrompt("Passphrase: ")
 		if err != nil {
 			return otto.FalseValue()
 		}
-		passwd2, err := PromptPassword("Repeat passphrase: ", true)
+		passwd2, err := Stdin.PasswordPrompt("Repeat passphrase: ")
 		if err != nil {
 			return otto.FalseValue()
 		}
-- 
cgit v1.2.3