Browse Source

added attach over ipc command

Bas van Kervel 10 years ago
parent
commit
f202563777
8 changed files with 238 additions and 35 deletions
  1. 64 25
      cmd/geth/js.go
  2. 46 0
      cmd/geth/main.go
  3. 25 2
      rpc/comms/comms.go
  4. 1 2
      rpc/comms/http.go
  5. 18 0
      rpc/comms/inproc.go
  6. 64 4
      rpc/comms/ipc.go
  7. 11 1
      rpc/comms/ipc_unix.go
  8. 9 1
      rpc/comms/ipc_windows.go

+ 64 - 25
cmd/geth/js.go

@@ -26,6 +26,8 @@ import (
 	"path/filepath"
 	"strings"
 
+	"sort"
+
 	"github.com/ethereum/go-ethereum/cmd/utils"
 	"github.com/ethereum/go-ethereum/common"
 	"github.com/ethereum/go-ethereum/common/docserver"
@@ -36,7 +38,6 @@ import (
 	"github.com/ethereum/go-ethereum/rpc/api"
 	"github.com/ethereum/go-ethereum/rpc/codec"
 	"github.com/ethereum/go-ethereum/rpc/comms"
-	"github.com/ethereum/go-ethereum/rpc/shared"
 	"github.com/ethereum/go-ethereum/xeth"
 	"github.com/peterh/liner"
 	"github.com/robertkrimen/otto"
@@ -137,6 +138,40 @@ func apiWordCompleter(line string, pos int) (head string, completions []string,
 	return begin, completionWords, end
 }
 
+func newLightweightJSRE(libPath string, client comms.EthereumClient, interactive bool, f xeth.Frontend) *jsre {
+	js := &jsre{ps1: "> "}
+	js.wait = make(chan *big.Int)
+	js.client = client
+
+	if f == nil {
+		f = js
+	}
+
+	// update state in separare forever blocks
+	js.re = re.New(libPath)
+	if err := js.apiBindings(f); 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(func(hist *os.File) { lr.ReadHistory(hist) })
+		lr.SetCtrlCAborts(true)
+		js.loadAutoCompletion()
+		lr.SetWordCompleter(apiWordCompleter)
+		lr.SetTabCompletionStyle(liner.TabPrints)
+		js.prompter = lr
+		js.atexit = func() {
+			js.withHistory(func(hist *os.File) { hist.Truncate(0); lr.WriteHistory(hist) })
+			lr.Close()
+			close(js.wait)
+		}
+	}
+	return js
+}
+
 func newJSRE(ethereum *eth.Ethereum, libPath, corsDomain string, client comms.EthereumClient, interactive bool, f xeth.Frontend) *jsre {
 	js := &jsre{ethereum: ethereum, ps1: "> "}
 	// set default cors domain used by startRpc from CLI flag
@@ -177,7 +212,7 @@ func newJSRE(ethereum *eth.Ethereum, libPath, corsDomain string, client comms.Et
 }
 
 func (self *jsre) loadAutoCompletion() {
-	if modules, err := self.suportedApis(); err == nil {
+	if modules, err := self.supportedApis(); err == nil {
 		loadedModulesMethods = make(map[string][]string)
 		for module, _ := range modules {
 			loadedModulesMethods[module] = api.AutoCompletion[module]
@@ -185,34 +220,33 @@ func (self *jsre) loadAutoCompletion() {
 	}
 }
 
-func (self *jsre) suportedApis() (map[string]string, error) {
-	req := shared.Request{
-		Id:      1,
-		Jsonrpc: "2.0",
-		Method:  "modules",
-	}
-
-	err := self.client.Send(&req)
-	if err != nil {
-		return nil, err
-	}
-
-	res, err := self.client.Recv()
-	if err != nil {
-		return nil, err
-	}
-
-	if sucRes, ok := res.(map[string]string); ok {
-		if err == nil {
-			return sucRes, nil
+// show summary of current geth instance
+func (self *jsre) welcome() {
+	self.re.Eval(`console.log('instance: ' + web3.version.client);`)
+	self.re.Eval(`console.log(' datadir: ' + admin.datadir);`)
+	self.re.Eval(`console.log("coinbase: " + eth.coinbase);`)
+	self.re.Eval(`var lastBlockTimestamp = 1000 * eth.getBlock(eth.blockNumber).timestamp`)
+	self.re.Eval(`console.log("at block: " + eth.blockNumber + " (" + new Date(lastBlockTimestamp).toLocaleDateString()
+		+ " " + new Date(lastBlockTimestamp).toLocaleTimeString() + ")");`)
+
+	if modules, err := self.supportedApis(); err == nil {
+		loadedModules := make([]string, 0)
+		for api, version := range modules {
+			loadedModules = append(loadedModules, fmt.Sprintf("%s:%s", api, version))
 		}
+		sort.Strings(loadedModules)
+
+		self.re.Eval(fmt.Sprintf("var modules = '%s';", strings.Join(loadedModules, " ")))
+		self.re.Eval(`console.log(" modules: " + modules);`)
 	}
+}
 
-	return nil, fmt.Errorf("Unable to determine supported API's")
+func (self *jsre) supportedApis() (map[string]string, error) {
+	return self.client.SupportedModules()
 }
 
 func (js *jsre) apiBindings(f xeth.Frontend) error {
-	apis, err := js.suportedApis()
+	apis, err := js.supportedApis()
 	if err != nil {
 		return err
 	}
@@ -366,7 +400,12 @@ func (self *jsre) interactive() {
 }
 
 func (self *jsre) withHistory(op func(*os.File)) {
-	hist, err := os.OpenFile(filepath.Join(self.ethereum.DataDir, "history"), os.O_RDWR|os.O_CREATE, os.ModePerm)
+	datadir := common.DefaultDataDir()
+	if self.ethereum != nil {
+		datadir = self.ethereum.DataDir
+	}
+
+	hist, err := os.OpenFile(filepath.Join(datadir, "history"), os.O_RDWR|os.O_CREATE, os.ModePerm)
 	if err != nil {
 		fmt.Printf("unable to open history file: %v\n", err)
 		return

+ 46 - 0
cmd/geth/main.go

@@ -204,6 +204,16 @@ nodes.
 The Geth console is an interactive shell for the JavaScript runtime environment
 which exposes a node admin interface as well as the Ðapp JavaScript API.
 See https://github.com/ethereum/go-ethereum/wiki/Javascipt-Console
+`},
+		{
+			Action: attach,
+			Name:   "attach",
+			Usage:  `Geth Console: interactive JavaScript environment`,
+			Description: `
+The Geth console is an interactive shell for the JavaScript runtime environment
+which exposes a node admin interface as well as the Ðapp JavaScript API.
+See https://github.com/ethereum/go-ethereum/wiki/Javascipt-Console.
+This command allows to open a console on a running geth node.
 `,
 		},
 		{
@@ -297,6 +307,40 @@ func run(ctx *cli.Context) {
 	ethereum.WaitForShutdown()
 }
 
+func attach(ctx *cli.Context) {
+	// Wrap the standard output with a colorified stream (windows)
+	if isatty.IsTerminal(os.Stdout.Fd()) {
+		if pr, pw, err := os.Pipe(); err == nil {
+			go io.Copy(colorable.NewColorableStdout(), pr)
+			os.Stdout = pw
+		}
+	}
+
+	var client comms.EthereumClient
+	var err error
+	if ctx.Args().Present() {
+		client, err = comms.ClientFromEndpoint(ctx.Args().First(), codec.JSON)
+	} else {
+		cfg := comms.IpcConfig{
+			Endpoint: ctx.GlobalString(utils.IPCPathFlag.Name),
+		}
+		client, err = comms.NewIpcClient(cfg, codec.JSON)
+	}
+
+	if err != nil {
+		utils.Fatalf("Unable to attach to geth node - %v", err)
+	}
+
+	repl := newLightweightJSRE(
+		ctx.String(utils.JSpathFlag.Name),
+		client,
+		true,
+		nil)
+
+	repl.welcome()
+	repl.interactive()
+}
+
 func console(ctx *cli.Context) {
 	// Wrap the standard output with a colorified stream (windows)
 	if isatty.IsTerminal(os.Stdout.Fd()) {
@@ -323,6 +367,8 @@ func console(ctx *cli.Context) {
 		true,
 		nil,
 	)
+
+	repl.welcome()
 	repl.interactive()
 
 	ethereum.Stop()

+ 25 - 2
rpc/comms/comms.go

@@ -4,12 +4,14 @@ import (
 	"io"
 	"net"
 
+	"fmt"
+	"strings"
+
 	"github.com/ethereum/go-ethereum/logger"
 	"github.com/ethereum/go-ethereum/logger/glog"
 	"github.com/ethereum/go-ethereum/rpc/api"
 	"github.com/ethereum/go-ethereum/rpc/codec"
 	"github.com/ethereum/go-ethereum/rpc/shared"
-	"strings"
 )
 
 const (
@@ -26,7 +28,7 @@ var (
 	// List with API's which are offered over thr HTTP/RPC interface by default
 	DefaultHttpRpcApis = strings.Join([]string{
 		api.DbApiName, api.EthApiName, api.NetApiName, api.Web3ApiName,
-		}, ",")
+	}, ",")
 )
 
 type EthereumClient interface {
@@ -36,6 +38,8 @@ type EthereumClient interface {
 	Send(interface{}) error
 	// Receive response
 	Recv() (interface{}, error)
+	// List with modules this client supports
+	SupportedModules() (map[string]string, error)
 }
 
 func handle(conn net.Conn, api api.EthereumApi, c codec.Codec) {
@@ -64,3 +68,22 @@ func handle(conn net.Conn, api api.EthereumApi, c codec.Codec) {
 		}
 	}
 }
+
+// Endpoint must be in the form of:
+// ${protocol}:${path}
+// e.g. ipc:/tmp/geth.ipc
+//      rpc:localhost:8545
+func ClientFromEndpoint(endpoint string, c codec.Codec) (EthereumClient, error) {
+	if strings.HasPrefix(endpoint, "ipc:") {
+		cfg := IpcConfig{
+			Endpoint: endpoint[4:],
+		}
+		return NewIpcClient(cfg, codec.JSON)
+	}
+
+	if strings.HasPrefix(endpoint, "rpc:") {
+
+	}
+
+	return nil, fmt.Errorf("Invalid endpoint")
+}

+ 1 - 2
rpc/comms/http.go

@@ -64,7 +64,6 @@ func StopHttp() {
 	}
 }
 
-
 type httpClient struct {
 	codec codec.ApiCoder
 }
@@ -86,4 +85,4 @@ func (self *httpClient) Send(req interface{}) error {
 
 func (self *httpClient) Recv() (interface{}, error) {
 	return nil, nil
-}
+}

+ 18 - 0
rpc/comms/inproc.go

@@ -51,3 +51,21 @@ func (self *InProcClient) Send(req interface{}) error {
 func (self *InProcClient) Recv() (interface{}, error) {
 	return self.lastRes, self.lastErr
 }
+
+func (self *InProcClient) SupportedModules() (map[string]string, error) {
+	req := shared.Request{
+		Id:      1,
+		Jsonrpc: "2.0",
+		Method:  "modules",
+	}
+
+	if res, err := self.api.Execute(&req); err == nil {
+		if result, ok := res.(map[string]string); ok {
+			return result, nil
+		}
+	} else {
+		return nil, err
+	}
+
+	return nil, fmt.Errorf("Invalid response")
+}

+ 64 - 4
rpc/comms/ipc.go

@@ -1,8 +1,13 @@
 package comms
 
 import (
+	"fmt"
+	"net"
+
 	"github.com/ethereum/go-ethereum/rpc/api"
 	"github.com/ethereum/go-ethereum/rpc/codec"
+	"github.com/ethereum/go-ethereum/rpc/shared"
+	"encoding/json"
 )
 
 type IpcConfig struct {
@@ -10,19 +15,74 @@ type IpcConfig struct {
 }
 
 type ipcClient struct {
-	codec codec.ApiCoder
+	endpoint string
+	codec    codec.Codec
+	coder    codec.ApiCoder
 }
 
 func (self *ipcClient) Close() {
-	self.codec.Close()
+	self.coder.Close()
 }
 
 func (self *ipcClient) Send(req interface{}) error {
-	return self.codec.WriteResponse(req)
+	var err error
+	if r, ok := req.(*shared.Request); ok {
+		if err = self.coder.WriteResponse(r); err != nil {
+			if _, ok := err.(*net.OpError); ok { // connection lost, retry once
+				if err = self.reconnect(); err == nil {
+					err = self.coder.WriteResponse(r)
+				}
+			}
+		}
+		return err
+	}
+
+	return fmt.Errorf("Invalid request (%T)", req)
 }
 
 func (self *ipcClient) Recv() (interface{}, error) {
-	return self.codec.ReadResponse()
+	res, err := self.coder.ReadResponse()
+	if err != nil {
+		return nil, err
+	}
+
+	if r, ok := res.(shared.SuccessResponse); ok {
+		return r.Result, nil
+	}
+
+	if r, ok := res.(shared.ErrorResponse); ok {
+		return r.Error, nil
+	}
+
+	return res, err
+}
+
+func (self *ipcClient) SupportedModules() (map[string]string, error) {
+	req := shared.Request{
+		Id:      1,
+		Jsonrpc: "2.0",
+		Method:  "modules",
+	}
+
+	if err := self.coder.WriteResponse(req); err != nil {
+		return nil, err
+	}
+
+	res, err := self.coder.ReadResponse()
+	if err != nil {
+		return nil, err
+	}
+
+	if sucRes, ok := res.(shared.SuccessResponse); ok {
+		data, _ := json.Marshal(sucRes.Result)
+		modules := make(map[string]string)
+		err = json.Unmarshal(data, &modules)
+		if err == nil {
+			return modules, nil
+		}
+	}
+
+	return nil, fmt.Errorf("Invalid response")
 }
 
 // Create a new IPC client, UNIX domain socket on posix, named pipe on Windows

+ 11 - 1
rpc/comms/ipc_unix.go

@@ -18,7 +18,17 @@ func newIpcClient(cfg IpcConfig, codec codec.Codec) (*ipcClient, error) {
 		return nil, err
 	}
 
-	return &ipcClient{codec.New(c)}, nil
+	return &ipcClient{cfg.Endpoint, codec, codec.New(c)}, nil
+}
+
+func (self *ipcClient) reconnect() error {
+	self.coder.Close()
+	c, err := net.DialUnix("unix", nil, &net.UnixAddr{self.endpoint, "unix"})
+	if err == nil {
+		self.coder = self.codec.New(c)
+	}
+
+	return err
 }
 
 func startIpc(cfg IpcConfig, codec codec.Codec, api api.EthereumApi) error {

+ 9 - 1
rpc/comms/ipc_windows.go

@@ -641,7 +641,15 @@ func newIpcClient(cfg IpcConfig, codec codec.Codec) (*ipcClient, error) {
 		return nil, err
 	}
 
-	return &ipcClient{codec.New(c)}, nil
+	return &ipcClient{cfg.Endpoint, codec, codec.New(c)}, nil
+}
+
+func (self *ipcClient) reconnect() error {
+	c, err := Dial(self.endpoint)
+	if err == nil {
+		self.coder = self.codec.New(c)
+	}
+	return err
 }
 
 func startIpc(cfg IpcConfig, codec codec.Codec, api api.EthereumApi) error {