Put the terminal back when a signal ends the mnemonic prompt (closes #91)
check / check (push) Waiting to run
check / check (push) Waiting to run
A SIGINT, SIGTERM or SIGHUP at the mnemonic prompt killed the tool with echo still off. The prompt now catches them with signals.Notify from before echo goes off, and reads the line in a goroutine. When one arrives it puts the terminal settings back, stops catching, and sends the tool the same signal again, so the tool is still ended by it (130 for SIGINT in a shell). A signal the tool was started with set to be ignored stays ignored. A test sends SIGINT at the prompt on a pseudo-terminal. Unchanged: the Windows prompt, which uses term.ReadPassword. Model: opus-5-5
This commit is contained in:
@@ -87,6 +87,9 @@ func init() {
|
||||
// still runs, and "age encrypt -o" and "age decrypt -o" while they
|
||||
// write a new file to rename over the named one, so the unfinished file
|
||||
// is removed.
|
||||
//
|
||||
// At the mnemonic prompt the signal is caught only to put the terminal
|
||||
// back, and then ends the tool as above.
|
||||
func Main() int {
|
||||
err := Root().Execute()
|
||||
if err == nil {
|
||||
|
||||
+80
-30
@@ -6,26 +6,25 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/term"
|
||||
"sneak.berlin/go/keyfunc/internal/mnemonic"
|
||||
)
|
||||
|
||||
// TestEndOfInputAtThePromptEndsTheTool runs "age pub" with no mnemonic
|
||||
// given and a terminal on all three streams, waits for the prompt, and
|
||||
// types Ctrl-D. The tool has to end with status 1, say that the
|
||||
// TestEndOfInputAtThePromptEndsTheTool starts "age pub" and, at the
|
||||
// prompt, types Ctrl-D. The tool has to end with status 1, say that the
|
||||
// mnemonic is empty, and leave the terminal set as it found it, so with
|
||||
// echo on again.
|
||||
//
|
||||
// pty.Open returns the two ends of a new terminal: tty is the end the
|
||||
// tool uses as its terminal, and ptmx the end the test reads what the
|
||||
// terminal shows from and types into. Reading ptmx stops at the
|
||||
// deadline, so a prompt that never comes fails the test instead of
|
||||
// hanging it.
|
||||
// terminal shows from and types into.
|
||||
func TestEndOfInputAtThePromptEndsTheTool(t *testing.T) {
|
||||
t.Setenv(mnemonic.Variable, "")
|
||||
t.Setenv(mnemonic.CommandVariable, "")
|
||||
@@ -36,33 +35,10 @@ func TestEndOfInputAtThePromptEndsTheTool(t *testing.T) {
|
||||
defer func() { _ = ptmx.Close() }()
|
||||
defer func() { _ = tty.Close() }()
|
||||
|
||||
require.NoError(t, ptmx.SetReadDeadline(time.Now().Add(10*time.Second)))
|
||||
|
||||
before, err := term.GetState(int(tty.Fd()))
|
||||
require.NoError(t, err)
|
||||
|
||||
//nolint:gosec // this test's own binary as the tool
|
||||
command := exec.CommandContext(t.Context(), os.Args[0], "age", "pub")
|
||||
|
||||
command.Env = append(os.Environ(), runAsTool+"=1")
|
||||
command.Stdin = tty
|
||||
command.Stdout = tty
|
||||
command.Stderr = tty
|
||||
|
||||
require.NoError(t, command.Start())
|
||||
|
||||
var (
|
||||
shown string
|
||||
char byte
|
||||
)
|
||||
|
||||
terminal := bufio.NewReader(ptmx)
|
||||
for !strings.HasSuffix(shown, "mnemonic: ") {
|
||||
char, err = terminal.ReadByte()
|
||||
require.NoError(t, err, "the tool did not prompt: %q", shown)
|
||||
|
||||
shown += string(char)
|
||||
}
|
||||
command, terminal := prompted(t, ptmx, tty)
|
||||
|
||||
_, err = ptmx.WriteString("\x04") // Ctrl-D
|
||||
require.NoError(t, err)
|
||||
@@ -81,3 +57,77 @@ func TestEndOfInputAtThePromptEndsTheTool(t *testing.T) {
|
||||
said, _ := io.ReadAll(terminal)
|
||||
require.Contains(t, string(said), mnemonic.ErrEmpty.Error())
|
||||
}
|
||||
|
||||
// TestASignalAtThePromptPutsTheTerminalBack starts "age pub" and, at
|
||||
// the prompt, once the tool has turned echo off, sends it SIGINT. The
|
||||
// tool has to be ended by SIGINT, which a shell reports as status 130,
|
||||
// and leave the terminal set as it found it, so with echo on again.
|
||||
func TestASignalAtThePromptPutsTheTerminalBack(t *testing.T) {
|
||||
t.Setenv(mnemonic.Variable, "")
|
||||
t.Setenv(mnemonic.CommandVariable, "")
|
||||
|
||||
ptmx, tty, err := pty.Open()
|
||||
require.NoError(t, err)
|
||||
|
||||
defer func() { _ = ptmx.Close() }()
|
||||
defer func() { _ = tty.Close() }()
|
||||
|
||||
before, err := term.GetState(int(tty.Fd()))
|
||||
require.NoError(t, err)
|
||||
|
||||
command, _ := prompted(t, ptmx, tty)
|
||||
|
||||
// The prompt is shown just before echo is turned off, and the
|
||||
// signal has to come while it is off.
|
||||
require.Eventually(t, func() bool {
|
||||
now, err := term.GetState(int(tty.Fd()))
|
||||
|
||||
return err == nil && !assert.ObjectsAreEqual(before, now)
|
||||
}, 5*time.Second, 5*time.Millisecond, "the tool did not turn echo off")
|
||||
|
||||
require.NoError(t, command.Process.Signal(syscall.SIGINT))
|
||||
waitForTool(t, "SIGINT at the prompt", command)
|
||||
|
||||
status, ok := command.ProcessState.Sys().(syscall.WaitStatus)
|
||||
require.True(t, ok)
|
||||
require.True(t, status.Signaled(), "the tool was not ended by the signal")
|
||||
require.Equal(t, syscall.SIGINT, status.Signal())
|
||||
|
||||
after, err := term.GetState(int(tty.Fd()))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, before, after, "the terminal settings were not restored")
|
||||
}
|
||||
|
||||
// prompted starts "age pub" as a subprocess, for a test that has set no
|
||||
// mnemonic, with the terminal's tty end on all three streams, and
|
||||
// returns once the tool has shown the prompt. The reader returned reads
|
||||
// what the terminal shows from its ptmx end. Reading ptmx stops at a
|
||||
// deadline, so a prompt that never comes fails the test instead of
|
||||
// hanging it.
|
||||
func prompted(t *testing.T, ptmx, tty *os.File) (*exec.Cmd, *bufio.Reader) {
|
||||
t.Helper()
|
||||
|
||||
require.NoError(t, ptmx.SetReadDeadline(time.Now().Add(10*time.Second)))
|
||||
|
||||
//nolint:gosec // this test's own binary as the tool
|
||||
command := exec.CommandContext(t.Context(), os.Args[0], "age", "pub")
|
||||
|
||||
command.Env = append(os.Environ(), runAsTool+"=1")
|
||||
command.Stdin = tty
|
||||
command.Stdout = tty
|
||||
command.Stderr = tty
|
||||
|
||||
require.NoError(t, command.Start())
|
||||
|
||||
var shown string
|
||||
|
||||
terminal := bufio.NewReader(ptmx)
|
||||
for !strings.HasSuffix(shown, "mnemonic: ") {
|
||||
char, err := terminal.ReadByte()
|
||||
require.NoError(t, err, "the tool did not prompt: %q", shown)
|
||||
|
||||
shown += string(char)
|
||||
}
|
||||
|
||||
return command, terminal
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user