1 Commits
Author SHA1 Message Date
sneak a6a9c7b18e Put the terminal back when a signal ends the mnemonic prompt (closes #91)
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. When one
arrives it puts the terminal settings back, stops catching, and sends
the tool the same signal again, so the tool is still ended by it (130
for SIGINT in a shell). A signal the tool was started with set to be
ignored stays ignored. A test sends SIGINT at the prompt on a
pseudo-terminal.

Unchanged: the Windows prompt, which uses term.ReadPassword.

Model: opus-5-5
2026-10-14 21:47:30 +00:00
6 changed files with 23 additions and 39 deletions
+3 -5
View File
@@ -65,12 +65,10 @@ calls into `internal/`. The packages there are:
and decrypts with it. and decrypts with it.
- `internal/childmnemonic` derives a child mnemonic from the main one using - `internal/childmnemonic` derives a child mnemonic from the main one using
BIP-85's own mnemonic application. BIP-85's own mnemonic application.
- `internal/signals` catches SIGINT, SIGTERM and SIGHUP for the commands that
clean up before they end, and for the mnemonic prompt, which puts the terminal
back before the signal ends the tool.
- `internal/cli` builds the cobra command tree and runs it. Under it, - `internal/cli` builds the cobra command tree and runs it. Under it,
`cli/options` holds the flags every command shares, and `cli/ssh`, `cli/age` `cli/options` holds the flags every command shares, `cli/signals` catches
and `cli/mnemonic` are the command groups. SIGINT, SIGTERM and SIGHUP for the commands that clean up before they end, and
`cli/ssh`, `cli/age` and `cli/mnemonic` are the command groups.
- `internal/bip39` is a copy of `github.com/tyler-smith/go-bip39` v1.1.0, - `internal/bip39` is a copy of `github.com/tyler-smith/go-bip39` v1.1.0,
trimmed to what keyfunc uses. trimmed to what keyfunc uses.
+1 -1
View File
@@ -16,8 +16,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"sneak.berlin/go/keyfunc/internal/agekey" "sneak.berlin/go/keyfunc/internal/agekey"
"sneak.berlin/go/keyfunc/internal/cli/options" "sneak.berlin/go/keyfunc/internal/cli/options"
"sneak.berlin/go/keyfunc/internal/cli/signals"
"sneak.berlin/go/keyfunc/internal/derive" "sneak.berlin/go/keyfunc/internal/derive"
"sneak.berlin/go/keyfunc/internal/signals"
) )
// ErrInterrupted is returned when SIGINT, SIGTERM or SIGHUP has been // ErrInterrupted is returned when SIGINT, SIGTERM or SIGHUP has been
@@ -1,9 +1,5 @@
// Package signals catches SIGINT, SIGTERM and SIGHUP, the signals that // Package signals catches the signals that end the tool, for the
// end the tool, for the commands that clean up before they end and for // commands that clean up before they end.
// the mnemonic prompt, which puts the terminal back before the signal
// ends the tool. Go keeps an inherited ignore only for SIGHUP and
// SIGINT, so a SIGTERM the tool was started with set to be ignored is
// not left ignored.
package signals package signals
import ( import (
@@ -15,8 +11,8 @@ import (
// Context is signal.NotifyContext for SIGINT, SIGTERM and SIGHUP: the // Context is signal.NotifyContext for SIGINT, SIGTERM and SIGHUP: the
// context it returns is cancelled when one of them arrives, and stop // context it returns is cancelled when one of them arrives, and stop
// stops catching them. It leaves out SIGHUP or SIGINT if the tool was // stops catching them. It leaves out any of the three the tool was
// started with it set to be ignored, as nohup does with SIGHUP, because // started with set to be ignored, as nohup does with SIGHUP, because
// catching a signal turns an ignored one back on and would end a run // catching a signal turns an ignored one back on and would end a run
// that was meant to survive it. // that was meant to survive it.
func Context(parent context.Context) (context.Context, context.CancelFunc) { func Context(parent context.Context) (context.Context, context.CancelFunc) {
@@ -41,8 +37,8 @@ func Notify(c chan<- os.Signal) {
} }
} }
// caught returns SIGINT, SIGTERM and SIGHUP, less any that // caught returns those of SIGINT, SIGTERM and SIGHUP that the tool was
// signal.Ignored reports as ignored. // not started with set to be ignored.
func caught() []os.Signal { func caught() []os.Signal {
endings := []os.Signal{syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP} endings := []os.Signal{syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP}
kept := make([]os.Signal, 0, len(endings)) kept := make([]os.Signal, 0, len(endings))
+1 -1
View File
@@ -15,8 +15,8 @@ import (
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"sneak.berlin/go/keyfunc/internal/cli/signals"
"sneak.berlin/go/keyfunc/internal/mnemonic" "sneak.berlin/go/keyfunc/internal/mnemonic"
"sneak.berlin/go/keyfunc/internal/signals"
) )
// Where the key goes on the host and what the file it arrives in is // Where the key goes on the host and what the file it arrives in is
+1 -1
View File
@@ -10,8 +10,8 @@ import (
"syscall" "syscall"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"sneak.berlin/go/keyfunc/internal/cli/signals"
"sneak.berlin/go/keyfunc/internal/mnemonic" "sneak.berlin/go/keyfunc/internal/mnemonic"
"sneak.berlin/go/keyfunc/internal/signals"
) )
// StatusError says the tool should end with the status ssh ended with. // StatusError says the tool should end with the status ssh ended with.
+10 -20
View File
@@ -11,7 +11,7 @@ import (
"os/signal" "os/signal"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"sneak.berlin/go/keyfunc/internal/signals" "sneak.berlin/go/keyfunc/internal/cli/signals"
) )
// readWithoutEcho reads one line from the terminal on standard input // readWithoutEcho reads one line from the terminal on standard input
@@ -21,9 +21,8 @@ import (
// of input and goes on waiting. // of input and goes on waiting.
// //
// Meanwhile SIGINT, SIGTERM and SIGHUP are caught, as signals.Notify // Meanwhile SIGINT, SIGTERM and SIGHUP are caught, as signals.Notify
// catches them. One that has arrived by the time the line is read puts // catches them. One that arrives puts the terminal back as it was and
// the terminal back as it was and then ends the tool, as it would have // then ends the tool, as it would have ended it if it were not caught.
// ended it if it were not caught.
func readWithoutEcho() (string, error) { func readWithoutEcho() (string, error) {
fd := int(os.Stdin.Fd()) fd := int(os.Stdin.Fd())
@@ -49,6 +48,11 @@ func readWithoutEcho() (string, error) {
return "", fmt.Errorf("turning echo off: %w", err) return "", fmt.Errorf("turning echo off: %w", err)
} }
restore := func() {
_ = unix.IoctlSetTermios(fd, ioctlWriteTermios, settings)
}
defer restore()
var ( var (
typed string typed string
failed error failed error
@@ -64,26 +68,12 @@ func readWithoutEcho() (string, error) {
close(read) close(read)
}() }()
var ending os.Signal
select { select {
case <-read: case <-read:
case ending = <-received: case ending := <-received:
} restore()
// 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) signal.Stop(received)
if len(received) > 0 {
ending = <-received
}
if ending != nil {
return "", endBy(ending) return "", endBy(ending)
} }