//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/cli/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 arrives 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) } restore := func() { _ = unix.IoctlSetTermios(fd, ioctlWriteTermios, settings) } defer restore() 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) }() select { case <-read: case ending := <-received: restore() signal.Stop(received) 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 {} }