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. Once the line is read or a signal arrives, it puts the terminal settings back and stops catching; if a signal has arrived by then, it sends the tool the same signal again, so the tool is still ended by it (130 for SIGINT in a shell). A test sends SIGINT at the prompt on a pseudo-terminal. The signals package moves from internal/cli to internal/signals, since the mnemonic prompt now uses it as well. Unchanged: the Windows prompt, which uses term.ReadPassword. Model: opus-5-5
This commit is contained in:
@@ -8,8 +8,10 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"sneak.berlin/go/keyfunc/internal/signals"
|
||||
)
|
||||
|
||||
// readWithoutEcho reads one line from the terminal on standard input
|
||||
@@ -17,6 +19,11 @@ import (
|
||||
// input (Ctrl-D). The terminal is set as term.ReadPassword sets it;
|
||||
// ReadPassword itself is not used here because it never sees the end
|
||||
// of input and goes on waiting.
|
||||
//
|
||||
// Meanwhile SIGINT, SIGTERM and SIGHUP are caught, as signals.Notify
|
||||
// catches them. One that has arrived by the time the line is read puts
|
||||
// the terminal back as it was and then ends the tool, as it would have
|
||||
// ended it if it were not caught.
|
||||
func readWithoutEcho() (string, error) {
|
||||
fd := int(os.Stdin.Fd())
|
||||
|
||||
@@ -25,6 +32,13 @@ func readWithoutEcho() (string, error) {
|
||||
return "", fmt.Errorf("reading the terminal settings: %w", err)
|
||||
}
|
||||
|
||||
// Caught from before echo is turned off, so that no signal can end
|
||||
// the tool with echo off.
|
||||
received := make(chan os.Signal, 1)
|
||||
signals.Notify(received)
|
||||
|
||||
defer signal.Stop(received)
|
||||
|
||||
hidden := *settings
|
||||
hidden.Lflag &^= unix.ECHO
|
||||
hidden.Lflag |= unix.ICANON | unix.ISIG
|
||||
@@ -35,14 +49,63 @@ func readWithoutEcho() (string, error) {
|
||||
return "", fmt.Errorf("turning echo off: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = unix.IoctlSetTermios(fd, ioctlWriteTermios, settings) }()
|
||||
var (
|
||||
typed string
|
||||
failed error
|
||||
)
|
||||
|
||||
// With ICANON set, a read from the terminal returns at most one
|
||||
// line, so the buffer takes nothing past the newline.
|
||||
typed, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
if errors.Is(err, io.EOF) {
|
||||
read := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
// With ICANON set, a read from the terminal returns at most one
|
||||
// line, so the buffer takes nothing past the newline.
|
||||
typed, failed = bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
|
||||
close(read)
|
||||
}()
|
||||
|
||||
var ending os.Signal
|
||||
|
||||
select {
|
||||
case <-read:
|
||||
case ending = <-received:
|
||||
}
|
||||
|
||||
// The terminal is put back while the signals are still caught, so
|
||||
// that none can end the tool with echo off. Stop returns only once
|
||||
// every signal the tool has received has been handed over, so one
|
||||
// that came as the line was read is in received by then.
|
||||
_ = unix.IoctlSetTermios(fd, ioctlWriteTermios, settings)
|
||||
|
||||
signal.Stop(received)
|
||||
|
||||
if len(received) > 0 {
|
||||
ending = <-received
|
||||
}
|
||||
|
||||
if ending != nil {
|
||||
return "", endBy(ending)
|
||||
}
|
||||
|
||||
if errors.Is(failed, io.EOF) {
|
||||
return typed, nil
|
||||
}
|
||||
|
||||
return typed, err
|
||||
return typed, failed
|
||||
}
|
||||
|
||||
// endBy sends the tool the signal again, now that nothing catches it,
|
||||
// so that it ends the tool, and waits for that. It returns only if the
|
||||
// signal could not be sent.
|
||||
func endBy(ending os.Signal) error {
|
||||
tool, err := os.FindProcess(os.Getpid())
|
||||
if err == nil {
|
||||
err = tool.Signal(ending)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("ending the tool by %s: %w", ending, err)
|
||||
}
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user