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
112 lines
2.7 KiB
Go
112 lines
2.7 KiB
Go
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos
|
|
|
|
package mnemonic
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"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
|
|
// with echo turned off. The line ends at a newline or at the end of
|
|
// 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())
|
|
|
|
settings, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
|
|
if err != nil {
|
|
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
|
|
hidden.Iflag |= unix.ICRNL
|
|
|
|
err = unix.IoctlSetTermios(fd, ioctlWriteTermios, &hidden)
|
|
if err != nil {
|
|
return "", fmt.Errorf("turning echo off: %w", err)
|
|
}
|
|
|
|
var (
|
|
typed string
|
|
failed error
|
|
)
|
|
|
|
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, 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 {}
|
|
}
|