Signals end every command, not only ssh to and ssh install (closes #48)
check / check (push) Failing after 2s
check / check (push) Failing after 2s
SIGINT, SIGTERM and SIGHUP were caught for the whole run, so the mnemonic prompt waited for Enter and an interrupted `age encrypt -o` put the encryption of the cut-off input in place. Now they end the tool at once, except where a command cleans up first: `ssh to` and `ssh install` while ssh or sftp runs, and `age encrypt -o` and `age decrypt -o` while they write. A signal those two have received when their input ends removes the unfinished file and exits 1; a later one leaves the whole file in place. The tool stays on the main thread, where Linux delivers the signal first. Signals the tool was started ignoring stay ignored, so a run under nohup survives a hangup. Model: opus-5-5
This commit is contained in:
+60
-26
@@ -3,17 +3,27 @@
|
||||
package age
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"sneak.berlin/go/keyfunc/internal/agekey"
|
||||
"sneak.berlin/go/keyfunc/internal/cli/options"
|
||||
"sneak.berlin/go/keyfunc/internal/cli/signals"
|
||||
"sneak.berlin/go/keyfunc/internal/derive"
|
||||
)
|
||||
|
||||
// ErrInterrupted is returned when SIGINT, SIGTERM or SIGHUP has been
|
||||
// received by the time the work writing the file --output names ends.
|
||||
var ErrInterrupted = errors.New(
|
||||
"interrupted by a signal; the output file was left as it was",
|
||||
)
|
||||
|
||||
// Command returns the age command and everything under it.
|
||||
func Command() *cobra.Command {
|
||||
group := &cobra.Command{
|
||||
@@ -128,8 +138,9 @@ func runDecrypt(cmd *cobra.Command, args []string) error {
|
||||
return through(cmd, args, key.Decrypt)
|
||||
}
|
||||
|
||||
// through opens the input and the output the arguments ask for, hands
|
||||
// them to the work, and finishes the output afterwards either way.
|
||||
// through opens the input the arguments ask for and hands it to the
|
||||
// work, with the file --output names to write to, or the command's own
|
||||
// output when it names none.
|
||||
func through(
|
||||
cmd *cobra.Command, args []string,
|
||||
work func(io.Writer, io.Reader) error,
|
||||
@@ -141,14 +152,16 @@ func through(
|
||||
|
||||
defer closeSrc()
|
||||
|
||||
dst, done, err := output(cmd)
|
||||
name, err := cmd.Flags().GetString("output")
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("reading the output file: %w", err)
|
||||
}
|
||||
|
||||
err = work(dst, src)
|
||||
if name == "" {
|
||||
return work(cmd.OutOrStdout(), src)
|
||||
}
|
||||
|
||||
return done(err)
|
||||
return output(name, src, work)
|
||||
}
|
||||
|
||||
// input returns what to read from: the named file, or the command's
|
||||
@@ -167,35 +180,56 @@ func input(cmd *cobra.Command, args []string) (io.Reader, func(), error) {
|
||||
return file, func() { _ = file.Close() }, nil
|
||||
}
|
||||
|
||||
// output returns what to write to: a new file beside the one --output
|
||||
// names, or the command's own output when it names none. The second
|
||||
// result finishes the write, and is given whatever the work returned:
|
||||
// the new file takes the named file's place only when the work
|
||||
// succeeded, so a file that is already there survives a run that
|
||||
// failed.
|
||||
func output(cmd *cobra.Command) (io.Writer, func(error) error, error) {
|
||||
name, err := cmd.Flags().GetString("output")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("reading the output file: %w", err)
|
||||
}
|
||||
// output has the work write a new file beside the named one, and puts
|
||||
// the new file in the named file's place only when the work succeeded,
|
||||
// so a file that is already there survives a run that failed.
|
||||
//
|
||||
// Meanwhile SIGINT, SIGTERM and SIGHUP are caught, as signals.Context
|
||||
// does. One the tool has received by the time the work ends wins: the
|
||||
// new file is removed and ErrInterrupted returned, at once if the work
|
||||
// is still running, without waiting for it, since it may be blocked
|
||||
// reading its input.
|
||||
func output(
|
||||
name string, src io.Reader, work func(io.Writer, io.Reader) error,
|
||||
) error {
|
||||
// received is registered before the context, so it gets every
|
||||
// signal the context gets.
|
||||
received := make(chan os.Signal, 1)
|
||||
signals.Notify(received)
|
||||
|
||||
if name == "" {
|
||||
return cmd.OutOrStdout(), func(failed error) error {
|
||||
return failed
|
||||
}, nil
|
||||
}
|
||||
defer signal.Stop(received)
|
||||
|
||||
// The context goes on catching the signals until the file is in
|
||||
// place or removed, so that a later one cannot end the tool with
|
||||
// the new file left beside the named one.
|
||||
interrupted, stop := signals.Context(context.Background())
|
||||
defer stop()
|
||||
|
||||
// The file is made in the same directory so that putting it in
|
||||
// place is a rename and never a copy, and it is readable only by
|
||||
// its owner, which is the mode it keeps once renamed.
|
||||
file, err := os.CreateTemp(filepath.Dir(name), filepath.Base(name)+".")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("creating a file beside %s: %w", name, err)
|
||||
return fmt.Errorf("creating a file beside %s: %w", name, err)
|
||||
}
|
||||
|
||||
return file, func(failed error) error {
|
||||
return finish(file, name, failed)
|
||||
}, nil
|
||||
worked := make(chan error, 1)
|
||||
|
||||
go func() { worked <- work(file, src) }()
|
||||
|
||||
select {
|
||||
case failed := <-worked:
|
||||
// Stop returns only once every signal the tool has received
|
||||
// has been handed over, so an empty received means none came.
|
||||
signal.Stop(received)
|
||||
|
||||
if len(received) == 0 {
|
||||
return finish(file, name, failed)
|
||||
}
|
||||
case <-interrupted.Done():
|
||||
}
|
||||
|
||||
return finish(file, name, ErrInterrupted)
|
||||
}
|
||||
|
||||
// finish closes the new file and puts it in the named file's place, or
|
||||
|
||||
Reference in New Issue
Block a user