github.com/tirogen/go-ethereum@v1.10.12-0.20221226051715-250cfede41b6/cmd/puppeth/wizard.go (about) 1 // Copyright 2017 The go-ethereum Authors 2 // This file is part of go-ethereum. 3 // 4 // go-ethereum is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // go-ethereum is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. 16 17 package main 18 19 import ( 20 "encoding/json" 21 "fmt" 22 "math/big" 23 "net" 24 "net/url" 25 "os" 26 "path/filepath" 27 "sort" 28 "strconv" 29 "strings" 30 "sync" 31 32 "github.com/peterh/liner" 33 "github.com/tirogen/go-ethereum/common" 34 "github.com/tirogen/go-ethereum/console/prompt" 35 "github.com/tirogen/go-ethereum/core" 36 "github.com/tirogen/go-ethereum/log" 37 "golang.org/x/term" 38 ) 39 40 // config contains all the configurations needed by puppeth that should be saved 41 // between sessions. 42 type config struct { 43 path string // File containing the configuration values 44 bootnodes []string // Bootnodes to always connect to by all nodes 45 ethstats string // Ethstats settings to cache for node deploys 46 47 Genesis *core.Genesis `json:"genesis,omitempty"` // Genesis block to cache for node deploys 48 Servers map[string][]byte `json:"servers,omitempty"` 49 } 50 51 // servers retrieves an alphabetically sorted list of servers. 52 func (c config) servers() []string { 53 servers := make([]string, 0, len(c.Servers)) 54 for server := range c.Servers { 55 servers = append(servers, server) 56 } 57 sort.Strings(servers) 58 59 return servers 60 } 61 62 // flush dumps the contents of config to disk. 63 func (c config) flush() { 64 os.MkdirAll(filepath.Dir(c.path), 0755) 65 66 out, _ := json.MarshalIndent(c, "", " ") 67 if err := os.WriteFile(c.path, out, 0644); err != nil { 68 log.Warn("Failed to save puppeth configs", "file", c.path, "err", err) 69 } 70 } 71 72 type wizard struct { 73 network string // Network name to manage 74 conf config // Configurations from previous runs 75 76 servers map[string]*sshClient // SSH connections to servers to administer 77 services map[string][]string // Ethereum services known to be running on servers 78 79 lock sync.Mutex // Lock to protect configs during concurrent service discovery 80 } 81 82 // prompts the user for input with the given prompt string. Returns when a value is entered. 83 // Causes the wizard to exit if ctrl-d is pressed 84 func promptInput(p string) string { 85 for { 86 text, err := prompt.Stdin.PromptInput(p) 87 if err != nil { 88 if err != liner.ErrPromptAborted { 89 log.Crit("Failed to read user input", "err", err) 90 } 91 } else { 92 return text 93 } 94 } 95 } 96 97 // read reads a single line from stdin, trimming if from spaces. 98 func (w *wizard) read() string { 99 text := promptInput("> ") 100 return strings.TrimSpace(text) 101 } 102 103 // readString reads a single line from stdin, trimming if from spaces, enforcing 104 // non-emptyness. 105 func (w *wizard) readString() string { 106 for { 107 text := promptInput("> ") 108 if text = strings.TrimSpace(text); text != "" { 109 return text 110 } 111 } 112 } 113 114 // readDefaultString reads a single line from stdin, trimming if from spaces. If 115 // an empty line is entered, the default value is returned. 116 func (w *wizard) readDefaultString(def string) string { 117 text := promptInput("> ") 118 if text = strings.TrimSpace(text); text != "" { 119 return text 120 } 121 return def 122 } 123 124 // readDefaultYesNo reads a single line from stdin, trimming if from spaces and 125 // interpreting it as a 'yes' or a 'no'. If an empty line is entered, the default 126 // value is returned. 127 func (w *wizard) readDefaultYesNo(def bool) bool { 128 for { 129 text := promptInput("> ") 130 if text = strings.ToLower(strings.TrimSpace(text)); text == "" { 131 return def 132 } 133 if text == "y" || text == "yes" { 134 return true 135 } 136 if text == "n" || text == "no" { 137 return false 138 } 139 log.Error("Invalid input, expected 'y', 'yes', 'n', 'no' or empty") 140 } 141 } 142 143 // readURL reads a single line from stdin, trimming if from spaces and trying to 144 // interpret it as a URL (http, https or file). 145 func (w *wizard) readURL() *url.URL { 146 for { 147 text := promptInput("> ") 148 uri, err := url.Parse(strings.TrimSpace(text)) 149 if err != nil { 150 log.Error("Invalid input, expected URL", "err", err) 151 continue 152 } 153 return uri 154 } 155 } 156 157 // readInt reads a single line from stdin, trimming if from spaces, enforcing it 158 // to parse into an integer. 159 func (w *wizard) readInt() int { 160 for { 161 text := promptInput("> ") 162 if text = strings.TrimSpace(text); text == "" { 163 continue 164 } 165 val, err := strconv.Atoi(strings.TrimSpace(text)) 166 if err != nil { 167 log.Error("Invalid input, expected integer", "err", err) 168 continue 169 } 170 return val 171 } 172 } 173 174 // readDefaultInt reads a single line from stdin, trimming if from spaces, enforcing 175 // it to parse into an integer. If an empty line is entered, the default value is 176 // returned. 177 func (w *wizard) readDefaultInt(def int) int { 178 for { 179 text := promptInput("> ") 180 if text = strings.TrimSpace(text); text == "" { 181 return def 182 } 183 val, err := strconv.Atoi(strings.TrimSpace(text)) 184 if err != nil { 185 log.Error("Invalid input, expected integer", "err", err) 186 continue 187 } 188 return val 189 } 190 } 191 192 // readDefaultBigInt reads a single line from stdin, trimming if from spaces, 193 // enforcing it to parse into a big integer. If an empty line is entered, the 194 // default value is returned. 195 func (w *wizard) readDefaultBigInt(def *big.Int) *big.Int { 196 for { 197 text := promptInput("> ") 198 if text = strings.TrimSpace(text); text == "" { 199 return def 200 } 201 val, ok := new(big.Int).SetString(text, 0) 202 if !ok { 203 log.Error("Invalid input, expected big integer") 204 continue 205 } 206 return val 207 } 208 } 209 210 // readDefaultFloat reads a single line from stdin, trimming if from spaces, enforcing 211 // it to parse into a float. If an empty line is entered, the default value is returned. 212 func (w *wizard) readDefaultFloat(def float64) float64 { 213 for { 214 text := promptInput("> ") 215 if text = strings.TrimSpace(text); text == "" { 216 return def 217 } 218 val, err := strconv.ParseFloat(strings.TrimSpace(text), 64) 219 if err != nil { 220 log.Error("Invalid input, expected float", "err", err) 221 continue 222 } 223 return val 224 } 225 } 226 227 // readPassword reads a single line from stdin, trimming it from the trailing new 228 // line and returns it. The input will not be echoed. 229 func (w *wizard) readPassword() string { 230 fmt.Printf("> ") 231 text, err := term.ReadPassword(int(os.Stdin.Fd())) 232 if err != nil { 233 log.Crit("Failed to read password", "err", err) 234 } 235 fmt.Println() 236 return string(text) 237 } 238 239 // readAddress reads a single line from stdin, trimming if from spaces and converts 240 // it to an Ethereum address. 241 func (w *wizard) readAddress() *common.Address { 242 for { 243 text := promptInput("> 0x") 244 if text = strings.TrimSpace(text); text == "" { 245 return nil 246 } 247 // Make sure it looks ok and return it if so 248 if len(text) != 40 { 249 log.Error("Invalid address length, please retry") 250 continue 251 } 252 bigaddr, _ := new(big.Int).SetString(text, 16) 253 address := common.BigToAddress(bigaddr) 254 return &address 255 } 256 } 257 258 // readDefaultAddress reads a single line from stdin, trimming if from spaces and 259 // converts it to an Ethereum address. If an empty line is entered, the default 260 // value is returned. 261 func (w *wizard) readDefaultAddress(def common.Address) common.Address { 262 for { 263 // Read the address from the user 264 text := promptInput("> 0x") 265 if text = strings.TrimSpace(text); text == "" { 266 return def 267 } 268 // Make sure it looks ok and return it if so 269 if len(text) != 40 { 270 log.Error("Invalid address length, please retry") 271 continue 272 } 273 bigaddr, _ := new(big.Int).SetString(text, 16) 274 return common.BigToAddress(bigaddr) 275 } 276 } 277 278 // readJSON reads a raw JSON message and returns it. 279 func (w *wizard) readJSON() string { 280 var blob json.RawMessage 281 282 for { 283 text := promptInput("> ") 284 reader := strings.NewReader(text) 285 if err := json.NewDecoder(reader).Decode(&blob); err != nil { 286 log.Error("Invalid JSON, please try again", "err", err) 287 continue 288 } 289 return string(blob) 290 } 291 } 292 293 // readIPAddress reads a single line from stdin, trimming if from spaces and 294 // returning it if it's convertible to an IP address. The reason for keeping 295 // the user input format instead of returning a Go net.IP is to match with 296 // weird formats used by ethstats, which compares IPs textually, not by value. 297 func (w *wizard) readIPAddress() string { 298 for { 299 // Read the IP address from the user 300 fmt.Printf("> ") 301 text := promptInput("> ") 302 if text = strings.TrimSpace(text); text == "" { 303 return "" 304 } 305 // Make sure it looks ok and return it if so 306 if ip := net.ParseIP(text); ip == nil { 307 log.Error("Invalid IP address, please retry") 308 continue 309 } 310 return text 311 } 312 }