//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 {} }