Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 48 additions & 20 deletions cmd/XDC/consolecmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package main

import (
"fmt"
"net/url"
"os"
"os/signal"
"path/filepath"
Expand Down Expand Up @@ -85,10 +86,11 @@ func localConsole(ctx *cli.Context) error {
utils.Fatalf("Failed to attach to the inproc XDC: %v", err)
}
config := console.Config{
DataDir: utils.MakeDataDir(ctx),
DocRoot: ctx.String(utils.JSpathFlag.Name),
Client: client,
Preload: utils.MakeConsolePreloads(ctx),
DataDir: utils.MakeDataDir(ctx),
DocRoot: ctx.String(utils.JSpathFlag.Name),
Client: client,
LocalTransport: true,
Preload: utils.MakeConsolePreloads(ctx),
}

console, err := console.New(config)
Expand Down Expand Up @@ -132,15 +134,16 @@ func remoteConsole(ctx *cli.Context) error {
endpoint = fmt.Sprintf("%s/XDC.ipc", path)
}

client, err := dialRPC(endpoint)
client, localTransport, err := dialRPC(endpoint)
if err != nil {
utils.Fatalf("Unable to attach to remote XDC: %v", err)
}
config := console.Config{
DataDir: utils.MakeDataDir(ctx),
DocRoot: ctx.String(utils.JSpathFlag.Name),
Client: client,
Preload: utils.MakeConsolePreloads(ctx),
DataDir: utils.MakeDataDir(ctx),
DocRoot: ctx.String(utils.JSpathFlag.Name),
Client: client,
LocalTransport: localTransport,
Preload: utils.MakeConsolePreloads(ctx),
}

console, err := console.New(config)
Expand All @@ -164,15 +167,39 @@ func remoteConsole(ctx *cli.Context) error {
// dialRPC returns a RPC client which connects to the given endpoint.
// The check for empty endpoint implements the defaulting logic
// for "XDC attach" and "XDC monitor" with no argument.
func dialRPC(endpoint string) (*rpc.Client, error) {
func dialRPC(endpoint string) (*rpc.Client, bool, error) {
endpoint, localTransport := resolveConsoleEndpoint(endpoint)
client, err := rpc.Dial(endpoint)
return client, localTransport, err
}

func resolveConsoleEndpoint(endpoint string) (string, bool) {
if endpoint == "" {
endpoint = node.DefaultIPCEndpoint(clientIdentifier)
} else if strings.HasPrefix(endpoint, "rpc:") || strings.HasPrefix(endpoint, "ipc:") {
// Backwards compatibility with geth < 1.5 which required
// these prefixes.
endpoint = endpoint[4:]
return node.DefaultIPCEndpoint(clientIdentifier), true
}
if strings.HasPrefix(endpoint, "ipc:") {
// Backwards compatibility with geth < 1.5 which required these prefixes.
return endpoint[4:], true
}
// Backwards compatibility with geth < 1.5 which required this prefix.
// Strip the legacy prefix, then classify the resulting endpoint based
// on its actual transport instead of assuming it is local.
endpoint = strings.TrimPrefix(endpoint, "rpc:")
if endpoint == "stdio" {
return endpoint, false
}
u, err := url.Parse(endpoint)
if err != nil {
return endpoint, false
}
switch u.Scheme {
Comment thread
gzliudan marked this conversation as resolved.
case "http", "https", "ws", "wss", "stdio":
return endpoint, false
case "":
return endpoint, true
default:
return endpoint, false
}
return rpc.Dial(endpoint)
}

// ephemeralConsole starts a new XDC node, attaches an ephemeral JavaScript
Expand All @@ -190,10 +217,11 @@ func ephemeralConsole(ctx *cli.Context) error {
utils.Fatalf("Failed to attach to the inproc XDC: %v", err)
}
config := console.Config{
DataDir: utils.MakeDataDir(ctx),
DocRoot: ctx.String(utils.JSpathFlag.Name),
Client: client,
Preload: utils.MakeConsolePreloads(ctx),
DataDir: utils.MakeDataDir(ctx),
DocRoot: ctx.String(utils.JSpathFlag.Name),
Client: client,
LocalTransport: true,
Preload: utils.MakeConsolePreloads(ctx),
}

console, err := console.New(config)
Expand Down
88 changes: 86 additions & 2 deletions cmd/XDC/consolecmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package main
import (
"crypto/rand"
"math/big"
"net"
"path/filepath"
"runtime"
"strconv"
Expand Down Expand Up @@ -96,7 +97,7 @@ func TestIPCAttachWelcome(t *testing.T) {

func TestHTTPAttachWelcome(t *testing.T) {
coinbase := "0x8605cdbbdb6d264aa742e77020dcbc58fcdce182"
port := strconv.Itoa(trulyRandInt(1024, 65536)) // Yeah, sometimes this will fail, sorry :P
port := strconv.Itoa(freeTCPPort(t))
datadir := t.TempDir()
XDC := runXDC(t,
"--datadir", datadir, "--XDCx-datadir", datadir+"/XDCx",
Expand All @@ -112,7 +113,7 @@ func TestHTTPAttachWelcome(t *testing.T) {

func TestWSAttachWelcome(t *testing.T) {
coinbase := "0x8605cdbbdb6d264aa742e77020dcbc58fcdce182"
port := strconv.Itoa(trulyRandInt(1024, 65536)) // Yeah, sometimes this will fail, sorry :P
port := strconv.Itoa(freeTCPPort(t))
datadir := t.TempDir()
XDC := runXDC(t,
"--datadir", datadir, "--XDCx-datadir", datadir+"/XDCx",
Expand Down Expand Up @@ -160,6 +161,89 @@ at block: 0 ({{niltime}}){{if ipc}}
attach.ExpectExit()
}

func TestResolveConsoleEndpoint(t *testing.T) {
tests := []struct {
name string
endpoint string
wantLocal bool
wantPrefix string
}{
{name: "default ipc endpoint", endpoint: "", wantLocal: true, wantPrefix: ""},
{name: "explicit ipc path", endpoint: "/tmp/XDC.ipc", wantLocal: true, wantPrefix: "/tmp/XDC.ipc"},
{name: "legacy ipc prefix", endpoint: "ipc:/tmp/XDC.ipc", wantLocal: true, wantPrefix: "/tmp/XDC.ipc"},
{name: "legacy rpc prefix", endpoint: "rpc:/tmp/XDC.ipc", wantLocal: true, wantPrefix: "/tmp/XDC.ipc"},
{name: "windows drive path stays unsupported", endpoint: `C:\\Users\\tester\\XDC.ipc`, wantLocal: false, wantPrefix: `C:\\Users\\tester\\XDC.ipc`},
{name: "windows drive slash path stays unsupported", endpoint: "C:/Users/tester/XDC.ipc", wantLocal: false, wantPrefix: "C:/Users/tester/XDC.ipc"},
{name: "legacy rpc windows drive path stays unsupported", endpoint: `rpc:C:\\Users\\tester\\XDC.ipc`, wantLocal: false, wantPrefix: `C:\\Users\\tester\\XDC.ipc`},
{name: "legacy rpc http prefix", endpoint: "rpc:http://localhost:8545", wantPrefix: "http://localhost:8545", wantLocal: false},
{name: "legacy rpc ws prefix", endpoint: "rpc:ws://localhost:8546", wantPrefix: "ws://localhost:8546", wantLocal: false},
{name: "stdio endpoint", endpoint: "stdio", wantLocal: false, wantPrefix: "stdio"},
{name: "legacy rpc stdio prefix", endpoint: "rpc:stdio", wantLocal: false, wantPrefix: "stdio"},
{name: "http endpoint", endpoint: "http://localhost:8545", wantLocal: false, wantPrefix: "http://localhost:8545"},
{name: "ws endpoint", endpoint: "ws://localhost:8546", wantLocal: false, wantPrefix: "ws://localhost:8546"},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
gotEndpoint, gotLocal := resolveConsoleEndpoint(test.endpoint)
if gotLocal != test.wantLocal {
t.Fatalf("unexpected local transport classification: got %v want %v", gotLocal, test.wantLocal)
}
if test.wantPrefix == "" {
if !strings.HasSuffix(gotEndpoint, "XDC.ipc") {
t.Fatalf("expected default IPC endpoint, got %q", gotEndpoint)
}
return
}
if gotEndpoint != test.wantPrefix {
t.Fatalf("unexpected resolved endpoint: got %q want %q", gotEndpoint, test.wantPrefix)
}
})
}
}

func TestDialRPCRejectsWindowsDrivePaths(t *testing.T) {
tests := []struct {
name string
endpoint string
}{
{name: "windows drive path", endpoint: `C:\\Users\\tester\\XDC.ipc`},
{name: "windows drive slash path", endpoint: "C:/Users/tester/XDC.ipc"},
{name: "legacy rpc windows drive path", endpoint: `rpc:C:\\Users\\tester\\XDC.ipc`},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
client, local, err := dialRPC(test.endpoint)
if client != nil {
client.Close()
t.Fatal("expected dialRPC to reject Windows drive-letter path")
}
if err == nil {
t.Fatal("expected dialRPC to fail for Windows drive-letter path")
}
if local {
t.Fatal("expected Windows drive-letter path to stay classified as non-local")
}
if !strings.Contains(err.Error(), `no known transport for URL scheme "c"`) {
t.Fatalf("unexpected dialRPC error: %v", err)
}
})
}
}

func freeTCPPort(t *testing.T) int {
t.Helper()

listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to allocate test port: %v", err)
}
defer listener.Close()

return listener.Addr().(*net.TCPAddr).Port
}

// trulyRandInt generates a crypto random integer used by the console tests to
// not clash network ports with other tests running cocurrently.
func trulyRandInt(lo, hi int) int {
Expand Down
19 changes: 19 additions & 0 deletions cmd/XDC/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ func TestMain(m *testing.M) {
func runXDC(t *testing.T, args ...string) *testXDC {
tt := &testXDC{}
tt.TestCmd = cmdtest.NewTestCmd(t, tt)
var extraArgs []string
if !hasArg(args, "--http-port") {
extraArgs = append(extraArgs, "--http-port", "0")
}
if !hasArg(args, "--ws-port") {
extraArgs = append(extraArgs, "--ws-port", "0")
}
if len(extraArgs) > 0 {
args = append(extraArgs, args...)
}
for i, arg := range args {
switch arg {
case "--datadir":
Expand All @@ -82,3 +92,12 @@ func runXDC(t *testing.T, args ...string) *testXDC {

return tt
}

func hasArg(args []string, want string) bool {
for _, arg := range args {
if arg == want {
return true
}
}
return false
}
91 changes: 70 additions & 21 deletions console/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,26 +51,28 @@ const DefaultPrompt = "> "
// Config is the collection of configurations to fine tune the behavior of the
// JavaScript console.
type Config struct {
DataDir string // Data directory to store the console history at
DocRoot string // Filesystem path from where to load JavaScript files from
Client *rpc.Client // RPC client to execute Ethereum requests through
Prompt string // Input prompt prefix string (defaults to DefaultPrompt)
Prompter UserPrompter // Input prompter to allow interactive user feedback (defaults to TerminalPrompter)
Printer io.Writer // Output writer to serialize any display strings to (defaults to os.Stdout)
Preload []string // Absolute paths to JavaScript files to preload
DataDir string // Data directory to store the console history at
DocRoot string // Filesystem path from where to load JavaScript files from
Client *rpc.Client // RPC client to execute Ethereum requests through
LocalTransport bool // Whether the console is attached over an in-process or IPC transport
Prompt string // Input prompt prefix string (defaults to DefaultPrompt)
Prompter UserPrompter // Input prompter to allow interactive user feedback (defaults to TerminalPrompter)
Printer io.Writer // Output writer to serialize any display strings to (defaults to os.Stdout)
Preload []string // Absolute paths to JavaScript files to preload
}

// Console is a JavaScript interpreted runtime environment. It is a fully fleged
// JavaScript console attached to a running node via an external or in-process RPC
// client.
type Console struct {
client *rpc.Client // RPC client to execute Ethereum requests through
jsre *jsre.JSRE // JavaScript runtime environment running the interpreter
prompt string // Input prompt prefix string
prompter UserPrompter // Input prompter to allow interactive user feedback
histPath string // Absolute path to the console scrollback history
history []string // Scroll history maintained by the console
printer io.Writer // Output writer to serialize any display strings to
client *rpc.Client // RPC client to execute Ethereum requests through
jsre *jsre.JSRE // JavaScript runtime environment running the interpreter
localTransport bool // Whether the connected transport is in-process or IPC
prompt string // Input prompt prefix string
prompter UserPrompter // Input prompter to allow interactive user feedback
histPath string // Absolute path to the console scrollback history
history []string // Scroll history maintained by the console
printer io.Writer // Output writer to serialize any display strings to
}

// New initializes a JavaScript interpreted runtime environment and sets defaults
Expand All @@ -89,12 +91,13 @@ func New(config Config) (*Console, error) {

// Initialize the console and return
console := &Console{
client: config.Client,
jsre: jsre.New(config.DocRoot, config.Printer),
prompt: config.Prompt,
prompter: config.Prompter,
printer: config.Printer,
histPath: filepath.Join(config.DataDir, HistoryFile),
client: config.Client,
jsre: jsre.New(config.DocRoot, config.Printer),
localTransport: config.LocalTransport,
prompt: config.Prompt,
prompter: config.Prompter,
printer: config.Printer,
histPath: filepath.Join(config.DataDir, HistoryFile),
}
if err := os.MkdirAll(config.DataDir, 0700); err != nil {
return nil, err
Expand Down Expand Up @@ -207,9 +210,41 @@ func (c *Console) initExtensions() error {
}
}
})
if !c.localTransport {
c.hideUnavailableDebugMethods()
}
return nil
}

func (c *Console) hideUnavailableDebugMethods() {
c.jsre.Do(func(vm *goja.Runtime) {
if _, err := vm.RunString(`
(function() {
function hideMethod(obj, hidden) {
if (obj == null) {
return;
}
Object.defineProperty(obj, hidden, {
value: undefined,
writable: true,
configurable: true,
enumerable: false
});
}

if (typeof debug !== "undefined") {
hideMethod(debug, "setHead");
}
if (typeof web3 !== "undefined" && web3 !== null) {
hideMethod(web3.debug, "setHead");
}
})();
`); err != nil {
panic(err)
}
})
}

// initAdmin creates additional admin APIs implemented by the bridge.
func (c *Console) initAdmin(vm *goja.Runtime, bridge *bridge) {
if admin := getObject(vm, "admin"); admin != nil {
Expand Down Expand Up @@ -260,7 +295,21 @@ func (c *Console) AutoCompleteInput(line string, pos int) (string, []string, str
start++
break
}
return line[:start], c.jsre.CompleteKeywords(line[start:pos]), line[pos:]
return line[:start], c.filterCompletions(c.jsre.CompleteKeywords(line[start:pos])), line[pos:]
}

func (c *Console) filterCompletions(completions []string) []string {
if c.localTransport {
return completions
}
filtered := completions[:0]
for _, completion := range completions {
if completion == "debug.setHead" || completion == "debug.setHead(" || completion == "debug.setHead." || completion == "web3.debug.setHead" || completion == "web3.debug.setHead(" || completion == "web3.debug.setHead." {
continue
}
filtered = append(filtered, completion)
}
return filtered
}

// Welcome show summary of current Geth instance and some metadata about the
Expand Down
Loading
Loading