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 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. Untested: a signal that comes just as the line is read. Model: opus-5-5
This commit was merged in pull request #92.
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