gitlab.com/aquachain/aquachain@v1.17.16-rc3.0.20221018032414-e3ddf1e1c055/opt/console/console.go (about)

     1  // Copyright 2018 The aquachain Authors
     2  // This file is part of the aquachain library.
     3  //
     4  // The aquachain library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser 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  // The aquachain library 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 Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the aquachain library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package console
    18  
    19  import (
    20  	"fmt"
    21  	"io"
    22  	"io/ioutil"
    23  	"os"
    24  	"os/signal"
    25  	"path/filepath"
    26  	"regexp"
    27  	"sort"
    28  	"strings"
    29  	"syscall"
    30  
    31  	colorable "github.com/mattn/go-colorable"
    32  	"github.com/peterh/liner"
    33  	"github.com/robertkrimen/otto"
    34  	"gitlab.com/aquachain/aquachain/internal/jsre"
    35  	"gitlab.com/aquachain/aquachain/internal/web3ext"
    36  	rpc "gitlab.com/aquachain/aquachain/rpc/rpcclient"
    37  )
    38  
    39  var (
    40  	passwordRegexp = regexp.MustCompile(`personal.[nus]`)
    41  	onlyWhitespace = regexp.MustCompile(`^\s*$`)
    42  	exit           = regexp.MustCompile(`^\s*exit\s*;*\s*$`)
    43  	help           = regexp.MustCompile(`^\s*help\s*;*\s*$`)
    44  	sendline       = regexp.MustCompile(`^\s*send\s*;*\s*$`)
    45  )
    46  
    47  // HistoryFile is the file within the data directory to store input scrollback.
    48  const HistoryFile = "history"
    49  
    50  // DefaultPrompt is the default prompt line prefix to use for user input querying.
    51  const DefaultPrompt = "AQUA> "
    52  
    53  const helpText = `
    54  Web links:
    55  
    56  	Website: https://aquachain.github.io
    57  	Explorer: https://aquachain.github.io/explorer/
    58  	Wiki: http://github.com/aquanetwork/aquachain/wiki/Basics
    59  
    60  Common AQUA console commands::
    61  
    62  	New address:              personal.newAccount()
    63  	Import private key:       personal.importRawKey('the private key')
    64  	Get balance:              aqua.balance(aqua.coinbase)
    65  	Get all balances:         balance()
    66  	Get pending balances:     pending()
    67  	Send transaction:         send
    68  	List accounts:            aqua.accounts
    69  	Show Transaction:         aqua.getTransaction('the tx hash')
    70  	Show Block #1000:         aqua.getBlock('1000')
    71  	Show Latest:              aqua.getBlock('latest')
    72  	Convert 1 AQUA to wei:    web3.toWei(1)
    73  	Convert Hex to Decimal:   web3.toDecimal('0x123')
    74  	
    75  	Show peers:               admin.peers
    76  	Show Aquachain Node info: admin.nodeInfo
    77  	Show Scheduled hard forks:admin.nodeInfo.protocols
    78  
    79  
    80  In this javascript console, you can define variables and load script.
    81  
    82  	loadScript('filename.js')
    83  	block = aqua.getBlock
    84  	myBlock = block('0x92cd50f36edddd9347ec37ef93206135518acd4115941f6287ea00407f186e15')
    85  	tx = aqua.getTransaction('0x23eabf63f8da796e2e68cd2ae602c1b5a9cb8f9946ad9d87a9561924e3d20db8')
    86  	web3.fromWei(tx.value)
    87  
    88  You are in the AQUA console.
    89  Type any command, press TAB to autocomplete commands.
    90  `
    91  
    92  const logo = `                              _           _
    93    __ _  __ _ _   _  __ _  ___| |__   __ _(_)_ __
    94   / _ '|/ _' | | | |/ _' |/ __| '_ \ / _' | | '_ \
    95  | (_| | (_| | |_| | (_| | (__| | | | (_| | | | | |
    96   \__,_|\__, |\__,_|\__,_|\___|_| |_|\__,_|_|_| |_|
    97            |_|` + "\nUpdate Often! https://gitlab.com/aquachain/aquachain\n\n"
    98  
    99  // Config is the collection of configurations to fine tune the behavior of the
   100  // JavaScript console.
   101  type Config struct {
   102  	DataDir  string       // Data directory to store the console history at
   103  	DocRoot  string       // Filesystem path from where to load JavaScript files from
   104  	Client   *rpc.Client  // RPC client to execute Aquachain requests through
   105  	Prompt   string       // Input prompt prefix string (defaults to DefaultPrompt)
   106  	Prompter UserPrompter // Input prompter to allow interactive user feedback (defaults to TerminalPrompter)
   107  	Printer  io.Writer    // Output writer to serialize any display strings to (defaults to os.Stdout)
   108  	Preload  []string     // Absolute paths to JavaScript files to preload
   109  }
   110  
   111  // Console is a JavaScript interpreted runtime environment. It is a fully fleged
   112  // JavaScript console attached to a running node via an external or in-process RPC
   113  // client.
   114  type Console struct {
   115  	client   *rpc.Client  // RPC client to execute Aquachain requests through
   116  	jsre     *jsre.JSRE   // JavaScript runtime environment running the interpreter
   117  	prompt   string       // Input prompt prefix string
   118  	prompter UserPrompter // Input prompter to allow interactive user feedback
   119  	histPath string       // Absolute path to the console scrollback history
   120  	history  []string     // Scroll history maintained by the console
   121  	printer  io.Writer    // Output writer to serialize any display strings to
   122  }
   123  
   124  func New(config Config) (*Console, error) {
   125  	// Handle unset config values gracefully
   126  	if config.Prompter == nil {
   127  		config.Prompter = Stdin
   128  	}
   129  	if config.Prompt == "" {
   130  		config.Prompt = DefaultPrompt
   131  	}
   132  	if config.Printer == nil {
   133  		config.Printer = colorable.NewColorableStdout()
   134  	}
   135  	// Initialize the console and return
   136  	console := &Console{
   137  		client:   config.Client,
   138  		jsre:     jsre.New(config.DocRoot, config.Printer),
   139  		prompt:   config.Prompt,
   140  		prompter: config.Prompter,
   141  		printer:  config.Printer,
   142  		histPath: filepath.Join(config.DataDir, HistoryFile),
   143  	}
   144  	if err := os.MkdirAll(config.DataDir, 0700); err != nil {
   145  		return nil, err
   146  	}
   147  	if err := console.init(config.Preload); err != nil {
   148  		return nil, err
   149  	}
   150  	return console, nil
   151  }
   152  
   153  // init retrieves the available APIs from the remote RPC provider and initializes
   154  // the console's JavaScript namespaces based on the exposed modules.
   155  func (c *Console) init(preload []string) error {
   156  	// Initialize the JavaScript <-> Go RPC bridge
   157  	bridge := newBridge(c.client, c.prompter, c.printer)
   158  	c.jsre.Set("jeth", struct{}{})
   159  
   160  	jethObj, _ := c.jsre.Get("jeth")
   161  	jethObj.Object().Set("send", bridge.Send)
   162  	jethObj.Object().Set("sendAsync", bridge.Send)
   163  
   164  	consoleObj, _ := c.jsre.Get("console")
   165  	consoleObj.Object().Set("log", c.consoleOutput)
   166  	consoleObj.Object().Set("error", c.consoleOutput)
   167  
   168  	// Load all the internal utility JavaScript libraries
   169  	if err := c.jsre.Compile("bignumber.js", jsre.BigNumber_JS); err != nil {
   170  		return fmt.Errorf("bignumber.js: %v", err)
   171  	}
   172  	if err := c.jsre.Compile("web3.js", jsre.Web3_JS); err != nil {
   173  		return fmt.Errorf("web3.js: %v", err)
   174  	}
   175  	if _, err := c.jsre.Run("var Web3 = require('web3');"); err != nil {
   176  		return fmt.Errorf("web3 require: %v", err)
   177  	}
   178  	if _, err := c.jsre.Run("var web3 = new Web3(jeth);"); err != nil {
   179  		return fmt.Errorf("web3 provider: %v", err)
   180  	}
   181  	// Load the supported APIs into the JavaScript runtime environment
   182  	apis, err := c.client.SupportedModules()
   183  	if err != nil {
   184  		return fmt.Errorf("api modules: %v", err)
   185  	}
   186  	flatten := "var aqua = web3.aqua; var personal = web3.personal; "
   187  	for api := range apis {
   188  		if api == "web3" {
   189  			continue // manually mapped or ignore
   190  		}
   191  		if file, ok := web3ext.Modules[api]; ok {
   192  			// Load our extension for the module.
   193  			if err = c.jsre.Compile(fmt.Sprintf("%s.js", api), file); err != nil {
   194  				return fmt.Errorf("%s.js: %v", api, err)
   195  			}
   196  			flatten += fmt.Sprintf("var %s = web3.%s; ", api, api)
   197  		} else if obj, err := c.jsre.Run("web3." + api); err == nil && obj.IsObject() {
   198  			// Enable web3.js built-in extension if available.
   199  			flatten += fmt.Sprintf("var %s = web3.%s; ", api, api)
   200  		}
   201  	}
   202  	if _, err = c.jsre.Run(flatten); err != nil {
   203  		return fmt.Errorf("namespace flattening: %v", err)
   204  	}
   205  	// Initialize the global name register (disabled for now)
   206  	//c.jsre.Run(`var GlobalRegistrar = aqua.contract(` + registrar.GlobalRegistrarAbi + `);   registrar = GlobalRegistrar.at("` + registrar.GlobalRegistrarAddr + `");`)
   207  
   208  	// If the console is in interactive mode, instrument password related methods to query the user
   209  	if c.prompter != nil {
   210  		// Retrieve the account management object to instrument
   211  		personal, err := c.jsre.Get("personal")
   212  		if err != nil {
   213  			return err
   214  		}
   215  		// Override the openWallet, unlockAccount, newAccount and sign methods since
   216  		// these require user interaction. Assign these method in the Console the
   217  		// original web3 callbacks. These will be called by the jeth.* methods after
   218  		// they got the password from the user and send the original web3 request to
   219  		// the backend.
   220  		if obj := personal.Object(); obj != nil { // make sure the personal api is enabled over the interface
   221  			if _, err = c.jsre.Run(`jeth.openWallet = personal.openWallet;`); err != nil {
   222  				return fmt.Errorf("personal.openWallet: %v", err)
   223  			}
   224  			if _, err = c.jsre.Run(`jeth.unlockAccount = personal.unlockAccount;`); err != nil {
   225  				return fmt.Errorf("personal.unlockAccount: %v", err)
   226  			}
   227  			if _, err = c.jsre.Run(`jeth.newAccount = personal.newAccount;`); err != nil {
   228  				return fmt.Errorf("personal.newAccount: %v", err)
   229  			}
   230  			if _, err = c.jsre.Run(`jeth.sign = personal.sign;`); err != nil {
   231  				return fmt.Errorf("personal.sign: %v", err)
   232  			}
   233  			obj.Set("openWallet", bridge.OpenWallet)
   234  			obj.Set("unlockAccount", bridge.UnlockAccount)
   235  			obj.Set("newAccount", bridge.NewAccount)
   236  			obj.Set("sign", bridge.Sign)
   237  		}
   238  	}
   239  	// The admin.sleep and admin.sleepBlocks are offered by the console and not by the RPC layer.
   240  	admin, err := c.jsre.Get("admin")
   241  	if err != nil {
   242  		return err
   243  	}
   244  	if obj := admin.Object(); obj != nil { // make sure the admin api is enabled over the interface
   245  		obj.Set("sleepBlocks", bridge.SleepBlocks)
   246  		obj.Set("sleep", bridge.Sleep)
   247  		obj.Set("clearHistory", c.clearHistory)
   248  	}
   249  	// Preload any JavaScript files before starting the console
   250  	for _, path := range preload {
   251  		if err := c.jsre.Exec(path); err != nil {
   252  			failure := err.Error()
   253  			if ottoErr, ok := err.(*otto.Error); ok {
   254  				failure = ottoErr.String()
   255  			}
   256  			return fmt.Errorf("%s: %v", path, failure)
   257  		}
   258  	}
   259  	// Configure the console's input prompter for scrollback and tab completion
   260  	if c.prompter != nil {
   261  		if content, err := ioutil.ReadFile(c.histPath); err != nil {
   262  			c.prompter.SetHistory(nil)
   263  		} else {
   264  			c.history = strings.Split(string(content), "\n")
   265  			c.prompter.SetHistory(c.history)
   266  		}
   267  		c.prompter.SetWordCompleter(c.AutoCompleteInput)
   268  	}
   269  	return nil
   270  }
   271  
   272  func (c *Console) clearHistory() {
   273  	c.history = nil
   274  	c.prompter.ClearHistory()
   275  	if err := os.Remove(c.histPath); err != nil {
   276  		fmt.Fprintln(c.printer, "can't delete history file:", err)
   277  	} else {
   278  		fmt.Fprintln(c.printer, "history file deleted")
   279  	}
   280  }
   281  
   282  // consoleOutput is an override for the console.log and console.error methods to
   283  // stream the output into the configured output stream instead of stdout.
   284  func (c *Console) consoleOutput(call otto.FunctionCall) otto.Value {
   285  	output := []string{}
   286  	for _, argument := range call.ArgumentList {
   287  		output = append(output, fmt.Sprintf("%v", argument))
   288  	}
   289  	fmt.Fprintln(c.printer, strings.Join(output, " "))
   290  	return otto.Value{}
   291  }
   292  
   293  // AutoCompleteInput is a pre-assembled word completer to be used by the user
   294  // input prompter to provide hints to the user about the methods available.
   295  func (c *Console) AutoCompleteInput(line string, pos int) (string, []string, string) {
   296  	// No completions can be provided for empty inputs
   297  	if len(line) == 0 || pos == 0 {
   298  		return "", c.jsre.CompleteKeywords(""), ""
   299  	}
   300  	// Chunck data to relevant part for autocompletion
   301  	// E.g. in case of nested lines aqua.getBalance(aqua.coinb<tab><tab>
   302  	start := pos - 1
   303  	for ; start > 0; start-- {
   304  		// Skip all methods and namespaces (i.e. including the dot)
   305  		if line[start] == '.' || (line[start] >= 'a' && line[start] <= 'z') || (line[start] >= 'A' && line[start] <= 'Z') {
   306  			continue
   307  		}
   308  		// Handle web3 in a special way (i.e. other numbers aren't auto completed)
   309  		if start >= 3 && line[start-3:start] == "web3" {
   310  			start -= 3
   311  			continue
   312  		}
   313  		// We've hit an unexpected character, autocomplete form here
   314  		start++
   315  		break
   316  	}
   317  	return line[:start], c.jsre.CompleteKeywords(line[start:pos]), line[pos:]
   318  }
   319  
   320  // Welcome show summary of current Aquachain instance and some metadata about the
   321  // console's available modules.
   322  func (c *Console) Welcome() {
   323  	// friendly balance
   324  	c.jsre.Run(`
   325  function pending() {
   326  			var totalBal = 0;
   327  			for (var acctNum in aqua.accounts) {
   328  								var acct = aqua.accounts[acctNum];
   329  								var acctBal = aqua.balance(acct, 'pending');
   330  								totalBal += parseFloat(acctBal);
   331  								console.log("  aqua.accounts[" + acctNum + "]: \t" + acct + " \tbalance: " + acctBal + " AQUA");
   332  						}
   333  			console.log("Pending balance: " + totalBal + " AQUA");
   334  			return totalBal;
   335  };
   336  function balance() {
   337  			var totalBal = 0;
   338  			for (var acctNum in aqua.accounts) {
   339  								var acct = aqua.accounts[acctNum];
   340  								var acctBal = aqua.balance(acct, 'latest');
   341  								totalBal += parseFloat(acctBal);
   342  								console.log("  aqua.accounts[" + acctNum + "]: \t" + acct + " \tbalance: " + acctBal + " AQUA");
   343  						}
   344  			console.log("  Total balance: " + totalBal + " AQUA");
   345  			return totalBal;
   346  };
   347  	`)
   348  
   349  	// Print some generic Aquachain metadata
   350  	fmt.Fprintf(c.printer, "\nWelcome to the Aquachain JavaScript console!\n")
   351  	fmt.Fprintf(c.printer, logo)
   352  
   353  	c.jsre.Run(`
   354  		console.log("instance: " + web3.version.node);
   355  		console.log("coinbase: " + aqua.coinbase);
   356  		console.log("at block: " + aqua.blockNumber + " (" + new Date(1000 * aqua.getBlock(aqua.blockNumber).timestamp) + ")");
   357  		console.log("    algo: " + aqua.getBlock(aqua.blockNumber).version);
   358  		console.log(" datadir: " + admin.datadir);
   359  	`)
   360  
   361  	// List all the supported modules for the user to call
   362  	if apis, err := c.client.SupportedModules(); err == nil {
   363  		modules := make([]string, 0, len(apis))
   364  		for api, version := range apis {
   365  			if api == "eth" {
   366  				continue
   367  			}
   368  			modules = append(modules, fmt.Sprintf("%s:%s", api, version))
   369  		}
   370  		sort.Strings(modules)
   371  		fmt.Fprintln(c.printer, " modules:", strings.Join(modules, " "))
   372  	}
   373  	fmt.Fprintln(c.printer)
   374  }
   375  
   376  // Evaluate executes code and pretty prints the result to the specified output
   377  // stream.
   378  func (c *Console) Evaluate(statement string) error {
   379  	defer func() {
   380  		if r := recover(); r != nil {
   381  			fmt.Fprintf(c.printer, "[native] error: %v\n", r)
   382  		}
   383  	}()
   384  	return c.jsre.Evaluate(statement, c.printer)
   385  }
   386  
   387  // Interactive starts an interactive user session, where input is propted from
   388  // the configured user prompter.
   389  func (c *Console) Interactive() {
   390  	var (
   391  		prompt    = c.prompt          // Current prompt line (used for multi-line inputs)
   392  		indents   = 0                 // Current number of input indents (used for multi-line inputs)
   393  		input     = ""                // Current user input
   394  		scheduler = make(chan string) // Channel to send the next prompt on and receive the input
   395  	)
   396  	// Start a goroutine to listen for promt requests and send back inputs
   397  	go func() {
   398  		for {
   399  			// Read the next user input
   400  			line, err := c.prompter.PromptInput(<-scheduler)
   401  			if err != nil {
   402  				// In case of an error, either clear the prompt or fail
   403  				if err == liner.ErrPromptAborted { // ctrl-C
   404  					prompt, indents, input = c.prompt, 0, ""
   405  					scheduler <- ""
   406  					continue
   407  				}
   408  				close(scheduler)
   409  				return
   410  			}
   411  			// User input retrieved, send for interpretation and loop
   412  			scheduler <- line
   413  		}
   414  	}()
   415  	// Monitor Ctrl-C too in case the input is empty and we need to bail
   416  	abort := make(chan os.Signal, 1)
   417  	signal.Notify(abort, syscall.SIGINT, syscall.SIGTERM)
   418  
   419  	// Start sending prompts to the user and reading back inputs
   420  	for {
   421  		// Send the next prompt, triggering an input read and process the result
   422  		scheduler <- prompt
   423  		select {
   424  		case <-abort:
   425  			// User forcefully quite the console
   426  			fmt.Fprintln(c.printer, "caught interrupt, exiting")
   427  			return
   428  
   429  		case line, ok := <-scheduler:
   430  			// User input was returned by the prompter, handle special cases
   431  			if !ok || (indents <= 0 && exit.MatchString(line)) {
   432  				return
   433  			}
   434  			if onlyWhitespace.MatchString(line) {
   435  				continue
   436  			}
   437  			if !ok || (indents <= 0 && help.MatchString(line)) {
   438  				fmt.Fprintf(c.printer, "%s", helpText)
   439  				continue
   440  			}
   441  
   442  			// command: 'send'
   443  			if sendline.MatchString(line) {
   444  				err := handleSend(c)
   445  				if err != nil {
   446  					fmt.Fprintln(c.printer, "Error:", err)
   447  					continue
   448  				}
   449  				fmt.Fprintln(c.printer, "TX Sent!")
   450  				continue
   451  			}
   452  			// Append the line to the input and check for multi-line interpretation
   453  			input += line + "\n"
   454  
   455  			indents = countIndents(input)
   456  			if indents <= 0 {
   457  				prompt = c.prompt
   458  			} else {
   459  				prompt = strings.Repeat(".", indents*3) + " "
   460  			}
   461  			// If all the needed lines are present, save the command and run
   462  			if indents <= 0 {
   463  				if len(input) > 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) {
   464  					if command := strings.TrimSpace(input); len(c.history) == 0 || command != c.history[len(c.history)-1] {
   465  						c.history = append(c.history, command)
   466  						if c.prompter != nil {
   467  							c.prompter.AppendHistory(command)
   468  						}
   469  					}
   470  				}
   471  				c.Evaluate(input)
   472  				input = ""
   473  			}
   474  		}
   475  	}
   476  }
   477  
   478  func handleSend(c *Console) error {
   479  
   480  	cont, err := c.prompter.PromptConfirm("You are about to create a transaction from your Aquabase. Right?")
   481  	if err != nil {
   482  		return fmt.Errorf("input error: %v", err)
   483  	}
   484  	if !cont {
   485  		return fmt.Errorf("transaction canceled")
   486  	}
   487  	_, err = c.jsre.Run(`personal.unlockAccount(aqua.coinbase);`)
   488  	if err != nil {
   489  		return fmt.Errorf("error: %v", err)
   490  	}
   491  
   492  	amount, err := c.prompter.PromptInput("How much AQUA to send? For example: 0.1: ")
   493  	if err != nil {
   494  		return fmt.Errorf("input error: %v", err)
   495  	}
   496  
   497  	fmt.Fprintf(c.printer, "Send %q to whom?", amount)
   498  
   499  	destination, err := c.prompter.PromptInput("Where to send? With 0x prefix: ")
   500  	if err != nil {
   501  		return fmt.Errorf("input error: %v", err)
   502  	}
   503  
   504  	cont, err = c.prompter.PromptConfirm(fmt.Sprintf("Send %s to %s?", amount, destination))
   505  	if err != nil {
   506  		return fmt.Errorf("input error: %v", err)
   507  	}
   508  	if !cont {
   509  		return fmt.Errorf("transaction canceled")
   510  	}
   511  
   512  	fmt.Fprintln(c.printer, "Running:\n"+`aqua.sendTransaction({from: aqua.coinbase, to: '`+destination+`', value: web3.toWei(`+amount+`,'aqua')});`)
   513  	cont, err = c.prompter.PromptConfirm(fmt.Sprintf("REALLY Send %s to %s?", amount, destination))
   514  	if err != nil {
   515  		return fmt.Errorf("input error: %v", err)
   516  	}
   517  	if !cont {
   518  		return fmt.Errorf("transaction canceled")
   519  	}
   520  	if !strings.HasPrefix(destination, "0x") && !strings.HasPrefix(destination, "aqua.accounts[") {
   521  		return fmt.Errorf("does not have 0x prefix")
   522  	}
   523  	_, err = c.jsre.Run(`aqua.sendTransaction({from: aqua.coinbase, to: '` + destination + `', value: web3.toWei(` + amount + `,'aqua')});`)
   524  	if err != nil {
   525  		return fmt.Errorf("error: %v", err)
   526  	}
   527  
   528  	return nil
   529  }
   530  
   531  // countIndents returns the number of identations for the given input.
   532  // In case of invalid input such as var a = } the result can be negative.
   533  func countIndents(input string) int {
   534  	var (
   535  		indents     = 0
   536  		inString    = false
   537  		strOpenChar = ' '   // keep track of the string open char to allow var str = "I'm ....";
   538  		charEscaped = false // keep track if the previous char was the '\' char, allow var str = "abc\"def";
   539  	)
   540  
   541  	for _, c := range input {
   542  		switch c {
   543  		case '\\':
   544  			// indicate next char as escaped when in string and previous char isn't escaping this backslash
   545  			if !charEscaped && inString {
   546  				charEscaped = true
   547  			}
   548  		case '\'', '"':
   549  			if inString && !charEscaped && strOpenChar == c { // end string
   550  				inString = false
   551  			} else if !inString && !charEscaped { // begin string
   552  				inString = true
   553  				strOpenChar = c
   554  			}
   555  			charEscaped = false
   556  		case '{', '(':
   557  			if !inString { // ignore brackets when in string, allow var str = "a{"; without indenting
   558  				indents++
   559  			}
   560  			charEscaped = false
   561  		case '}', ')':
   562  			if !inString {
   563  				indents--
   564  			}
   565  			charEscaped = false
   566  		default:
   567  			charEscaped = false
   568  		}
   569  	}
   570  
   571  	return indents
   572  }
   573  
   574  // Execute runs the JavaScript file specified as the argument.
   575  func (c *Console) Execute(path string) error {
   576  	return c.jsre.Exec(path)
   577  }
   578  
   579  // Stop cleans up the console and terminates the runtime environment.
   580  func (c *Console) Stop(graceful bool) error {
   581  	if err := ioutil.WriteFile(c.histPath, []byte(strings.Join(c.history, "\n")), 0600); err != nil {
   582  		return err
   583  	}
   584  	if err := os.Chmod(c.histPath, 0600); err != nil { // Force 0600, even if it was different previously
   585  		return err
   586  	}
   587  	c.jsre.Stop(graceful)
   588  	return nil
   589  }