forked from cerc-io/plugeth
		
	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.
This commit is contained in:
		
							parent
							
								
									83877a0f9d
								
							
						
					
					
						commit
						dff9b4246f
					
				| @ -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) | ||||
| 	} | ||||
|  | ||||
| @ -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,30 +94,29 @@ 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) | ||||
| 	} | ||||
| 
 | ||||
| 	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) | ||||
| 		} | ||||
| 	} | ||||
| 	js.setupInput(stack.DataDir()) | ||||
| 	return js | ||||
| } | ||||
| 
 | ||||
| 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) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (self *jsre) batch(statement string) { | ||||
| 	err := self.re.EvalAndPrettyPrint(statement) | ||||
| 
 | ||||
| @ -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 = "" | ||||
|  | ||||
| @ -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) | ||||
| 	} | ||||
|  | ||||
| @ -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) | ||||
| 		} | ||||
|  | ||||
| @ -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.
 | ||||
|  | ||||
							
								
								
									
										98
									
								
								cmd/utils/input.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								cmd/utils/input.go
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
| } | ||||
| @ -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() | ||||
| 		} | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user