From eff02faba9db20c5434d9956ecb27cf487884020 Mon Sep 17 00:00:00 2001 From: mehdi salimi Date: Fri, 3 Jul 2026 00:16:22 +0330 Subject: [PATCH] console: print helpful error when attach stdin is not a terminal When geth attach is started without a TTY (e.g. via screen -X screen or ' for non-interactive use, +or run attach inside a TTY (e.g. screen -dmS name bash -lc 'geth attach ...').`) +} + // ephemeralConsole starts a new geth node, attaches an ephemeral JavaScript // console to it, executes each of the files specified as arguments and tears // everything down. diff --git a/cmd/geth/consolecmd_test.go b/cmd/geth/consolecmd_test.go index 2c8522d109..6ecac35139 100644 --- a/cmd/geth/consolecmd_test.go +++ b/cmd/geth/consolecmd_test.go @@ -119,7 +119,8 @@ func TestAttachWelcome(t *testing.T) { } func testAttachWelcome(t *testing.T, geth *testgeth, endpoint, apis string) { - // Attach to a running geth node and terminate immediately + // Attach to a running geth node with stdin closed, as happens when + // launched without a TTY (e.g. via GNU screen -X screen). attach := runGeth(t, "attach", endpoint) defer attach.ExpectExit() attach.CloseStdin() @@ -136,7 +137,7 @@ func testAttachWelcome(t *testing.T, geth *testgeth, endpoint, apis string) { attach.SetTemplateFunc("datadir", func() string { return geth.Datadir }) attach.SetTemplateFunc("apis", func() string { return apis }) - // Verify the actual welcome message to the required template + // Verify the welcome message and non-TTY hint are printed. attach.Expect(` Welcome to the Geth JavaScript console! @@ -146,9 +147,11 @@ at block: 0 ({{niltime}}){{if ipc}} modules: {{apis}} To exit, press ctrl-d or type exit -> {{.InputLine "exit" }} +> +Cannot start interactive console: input closed or not a terminal. +Hint: use 'geth attach --exec "eth.blockNumber" ' for non-interactive use, +or run attach inside a TTY (e.g. screen -dmS name bash -lc 'geth attach ...'). `) - attach.ExpectExit() } // trulyRandInt generates a crypto random integer used by the console tests to diff --git a/console/console.go b/console/console.go index 573d1da570..0b0e824c01 100644 --- a/console/console.go +++ b/console/console.go @@ -429,6 +429,11 @@ func (c *Console) Interactive() { prompt, indents, input = c.prompt, 0, "" continue } + if errors.Is(err, io.EOF) { + fmt.Fprintln(c.printer, "Cannot start interactive console: input closed or not a terminal.") + fmt.Fprintln(c.printer, "Hint: use 'geth attach --exec \"eth.blockNumber\" ' for non-interactive use,") + fmt.Fprintln(c.printer, "or run attach inside a TTY (e.g. screen -dmS name bash -lc 'geth attach ...').") + } return case line := <-inputLine: diff --git a/console/console_test.go b/console/console_test.go index d210a993c2..d567db6b02 100644 --- a/console/console_test.go +++ b/console/console_test.go @@ -20,6 +20,7 @@ import ( "bytes" "errors" "fmt" + "io" "os" "strings" "testing" @@ -72,6 +73,17 @@ func (p *hookedPrompter) AppendHistory(command string) {} func (p *hookedPrompter) ClearHistory() {} func (p *hookedPrompter) SetWordCompleter(completer prompt.WordCompleter) {} +// eofPrompter simulates a closed or non-terminal stdin by returning io.EOF. +type eofPrompter struct{} + +func (eofPrompter) PromptInput(string) (string, error) { return "", io.EOF } +func (eofPrompter) PromptPassword(string) (string, error) { return "", io.EOF } +func (eofPrompter) PromptConfirm(string) (bool, error) { return false, io.EOF } +func (eofPrompter) SetHistory([]string) {} +func (eofPrompter) AppendHistory(string) {} +func (eofPrompter) ClearHistory() {} +func (eofPrompter) SetWordCompleter(completer prompt.WordCompleter) {} + // tester is a console test environment for the console tests to operate on. type tester struct { workspace string @@ -85,6 +97,10 @@ type tester struct { // newTester creates a test environment based on which the console can operate. // Please ensure you call Close() on the returned tester to avoid leaks. func newTester(t *testing.T, confOverride func(*ethconfig.Config)) *tester { + return newTesterWithPrompter(t, &hookedPrompter{scheduler: make(chan string)}, confOverride) +} + +func newTesterWithPrompter(t *testing.T, prompter prompt.UserPrompter, confOverride func(*ethconfig.Config)) *tester { // Create a temporary storage for the node keys and initialize it workspace := t.TempDir() @@ -115,7 +131,6 @@ func newTester(t *testing.T, confOverride func(*ethconfig.Config)) *tester { client.Close() }) - prompter := &hookedPrompter{scheduler: make(chan string)} printer := new(bytes.Buffer) console, err := New(Config{ @@ -130,12 +145,16 @@ func newTester(t *testing.T, confOverride func(*ethconfig.Config)) *tester { t.Fatalf("failed to create JavaScript console: %v", err) } // Create the final tester and return + var input *hookedPrompter + if hp, ok := prompter.(*hookedPrompter); ok { + input = hp + } return &tester{ workspace: workspace, stack: stack, ethereum: ethBackend, console: console, - input: prompter, + input: input, output: printer, } } @@ -188,6 +207,19 @@ func TestEvaluate(t *testing.T) { } } +// Tests that Interactive exits with a helpful message when stdin is closed. +func TestInteractiveEOF(t *testing.T) { + tester := newTesterWithPrompter(t, eofPrompter{}, nil) + defer tester.Close(t) + + tester.console.Interactive() + + output := tester.output.String() + if want := "not a terminal"; !strings.Contains(output, want) { + t.Fatalf("console output missing EOF hint: have\n%s\nwant substring %s", output, want) + } +} + // Tests that the console can be used in interactive mode. func TestInteractive(t *testing.T) { // Create a tester and run an interactive console in the background