1 Commits
Author SHA1 Message Date
clawbot 8bb3d949d9 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 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
2026-10-15 01:35:42 +02:00
6 changed files with 39 additions and 23 deletions
+5 -3
View File
@@ -65,10 +65,12 @@ 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, `cli/signals` catches `cli/options` holds the flags every command shares, and `cli/ssh`, `cli/age`
SIGINT, SIGTERM and SIGHUP for the commands that clean up before they end, and and `cli/mnemonic` are the command groups.
`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 -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.
+21 -11
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/cli/signals" "sneak.berlin/go/keyfunc/internal/signals"
) )
// readWithoutEcho reads one line from the terminal on standard input // readWithoutEcho reads one line from the terminal on standard input
@@ -21,8 +21,9 @@ 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 arrives puts the terminal back as it was and // catches them. One that has arrived by the time the line is read puts
// then ends the tool, as it would have ended it if it were not caught. // 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) { func readWithoutEcho() (string, error) {
fd := int(os.Stdin.Fd()) fd := int(os.Stdin.Fd())
@@ -48,11 +49,6 @@ 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
@@ -68,12 +64,26 @@ 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() }
signal.Stop(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) return "", endBy(ending)
} }
@@ -1,5 +1,9 @@
// Package signals catches the signals that end the tool, for the // Package signals catches SIGINT, SIGTERM and SIGHUP, the signals that
// commands that clean up before they end. // end the tool, 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. 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 (
@@ -11,8 +15,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 any of the three the tool was // stops catching them. It leaves out SIGHUP or SIGINT if the tool was
// started with set to be ignored, as nohup does with SIGHUP, because // started with it 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) {
@@ -37,8 +41,8 @@ func Notify(c chan<- os.Signal) {
} }
} }
// caught returns those of SIGINT, SIGTERM and SIGHUP that the tool was // caught returns SIGINT, SIGTERM and SIGHUP, less any that
// not started with set to be ignored. // signal.Ignored reports as 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))