SIGINT, SIGTERM and SIGHUP are no longer caught for the whole run, so they end any command at once, the mnemonic prompt included. ssh to and ssh install catch them from once the mnemonic is read until their cleanup has run, so the agent socket or working directory is still removed.
age encrypt -o and age decrypt -o catch them while they write. A signal received by the time the work ends removes the unfinished file and exits 1: signal.Stop on a channel registered through signals.Notify returns once every received signal is handed over, so an empty channel means the file goes in place. cli keeps the tool on the main thread, where Linux delivers the signal first, so Ctrl-C on a pipeline has been received by then. No timed wait.
These four leave alone a signal the tool was started ignoring, as under nohup.
This is the plainer of the issue's two ways: stopping on a cancelled context would need the prompt read in a goroutine and the terminal restored by hand.
Judgement call: "puts no output file in place" holds for a signal received when the input ends; a later one leaves the whole file.
Judgement call: a signal at the prompt leaves terminal echo for the shell to restore.
Unverified: which thread macOS delivers the signal to.
Partially verified: the prompt case needs a terminal; checked by hand only.
Rule suppressed: gochecknoinits, since only an init stays on the main thread.
Model: opus-5-5
Implements https://git.eeqj.de/sneak/keyfunc/issues/48, to the design in https://git.eeqj.de/sneak/keyfunc/issues/48#issuecomment-122586.
SIGINT, SIGTERM and SIGHUP are no longer caught for the whole run, so they end any command at once, the mnemonic prompt included. `ssh to` and `ssh install` catch them from once the mnemonic is read until their cleanup has run, so the agent socket or working directory is still removed.
`age encrypt -o` and `age decrypt -o` catch them while they write. A signal received by the time the work ends removes the unfinished file and exits 1: `signal.Stop` on a channel registered through `signals.Notify` returns once every received signal is handed over, so an empty channel means the file goes in place. `cli` keeps the tool on the main thread, where Linux delivers the signal first, so Ctrl-C on a pipeline has been received by then. No timed wait.
These four leave alone a signal the tool was started ignoring, as under `nohup`.
This is the plainer of the issue's two ways: stopping on a cancelled context would need the prompt read in a goroutine and the terminal restored by hand.
- Judgement call: "puts no output file in place" holds for a signal received when the input ends; a later one leaves the whole file.
- Judgement call: a signal at the prompt leaves terminal echo for the shell to restore.
- Unverified: which thread macOS delivers the signal to.
- Partially verified: the prompt case needs a terminal; checked by hand only.
- Rule suppressed: `gochecknoinits`, since only an `init` stays on the main thread.
Model: opus-5-5
internal/cli/age/age.go (output, finish) and the new paragraph in README.md under Errors: when SIGINT, SIGTERM or SIGHUP ends an age encrypt -o or age decrypt -o run, the unfinished file beside the named one stays on disk (for example notes.txt.123456). For age decrypt -o that file holds the plaintext decrypted so far, under a name the user never gave and has no reason to look for. That is not acceptable for a tool that keeps secrets, and #48 asks that an interrupted age -o leave no file. Acceptable: while that file exists, those three signals remove it before the tool exits with a non-zero status (the blocked read does not have to stop for this); the encrypt test requires the directory to hold nothing new after the signal; the README says an interrupted -o run leaves no file and gives the exit status the tool then has.
PR body: about 270 words, over the limit of about 250. Trim it; the disclosure about the leftover file goes away with item 1.
Model: opus-5-5
1. `internal/cli/age/age.go` (`output`, `finish`) and the new paragraph in `README.md` under Errors: when SIGINT, SIGTERM or SIGHUP ends an `age encrypt -o` or `age decrypt -o` run, the unfinished file beside the named one stays on disk (for example `notes.txt.123456`). For `age decrypt -o` that file holds the plaintext decrypted so far, under a name the user never gave and has no reason to look for. That is not acceptable for a tool that keeps secrets, and https://git.eeqj.de/sneak/keyfunc/issues/48 asks that an interrupted `age -o` leave no file. Acceptable: while that file exists, those three signals remove it before the tool exits with a non-zero status (the blocked read does not have to stop for this); the encrypt test requires the directory to hold nothing new after the signal; the README says an interrupted `-o` run leaves no file and gives the exit status the tool then has.
2. PR body: about 270 words, over the limit of about 250. Trim it; the disclosure about the leftover file goes away with item 1.
Model: opus-5-5
Rebased onto next. While age encrypt -o or age decrypt -o writes, SIGINT, SIGTERM and SIGHUP now remove the unfinished file and end the tool with status 1, with a test for each and the README saying so; the PR body is trimmed.
Model: opus-5-5
Rebased onto `next`. While `age encrypt -o` or `age decrypt -o` writes, SIGINT, SIGTERM and SIGHUP now remove the unfinished file and end the tool with status 1, with a test for each and the README saying so; the PR body is trimmed.
Model: opus-5-5
internal/cli/age/age.go (output) and the README paragraph under Errors: Ctrl-C on producer | keyfunc age encrypt -o file, the case #48 is about, still sometimes puts the encryption of the cut-off input in place. The same Ctrl-C ends the producer, so the input ends just as the signal arrives, and the rename can happen before the goroutine that removes the file acts; the tool then exits 1, or 0 when Main returns before that goroutine's os.Exit(1), and a file already at the named path is replaced. age decrypt -o has the same race. The new tests signal the tool while its input stays open, so they never reach this. Acceptable: a signal that reaches the tool before the file is renamed always ends the run with no new file at the named path and a non-zero status, however the input ends; a test signals the tool and ends its input at the same moment, as Ctrl-C on a pipeline does, repeated often enough to catch the race.
Model: opus-5-5
1. `internal/cli/age/age.go` (`output`) and the README paragraph under Errors: Ctrl-C on `producer | keyfunc age encrypt -o file`, the case https://git.eeqj.de/sneak/keyfunc/issues/48 is about, still sometimes puts the encryption of the cut-off input in place. The same Ctrl-C ends the producer, so the input ends just as the signal arrives, and the rename can happen before the goroutine that removes the file acts; the tool then exits 1, or 0 when `Main` returns before that goroutine's `os.Exit(1)`, and a file already at the named path is replaced. `age decrypt -o` has the same race. The new tests signal the tool while its input stays open, so they never reach this. Acceptable: a signal that reaches the tool before the file is renamed always ends the run with no new file at the named path and a non-zero status, however the input ends; a test signals the tool and ends its input at the same moment, as Ctrl-C on a pipeline does, repeated often enough to catch the race.
Model: opus-5-5
The write now runs in the background while the tool waits for it to end or for a signal, and a signal up to a tenth of a second after the input ends still counts, so Ctrl-C on a pipeline leaves no file; a new test signals the tool and ends its input together, many times over, for both age encrypt -o and age decrypt -o.
Model: opus-5-5
The write now runs in the background while the tool waits for it to end or for a signal, and a signal up to a tenth of a second after the input ends still counts, so Ctrl-C on a pipeline leaves no file; a new test signals the tool and ends its input together, many times over, for both `age encrypt -o` and `age decrypt -o`.
Model: opus-5-5
internal/cli/age/age.go (output): it catches SIGINT, SIGTERM and SIGHUP even when the tool was started with one of them ignored, and catching a signal in Go turns an ignored one back on. So nohup keyfunc age decrypt -o out file.age & is ended by the hangup it was started to survive and throws away what it decrypted, and age encrypt -o run in the background of a script is ended by a Ctrl-C meant for the script. The same runs without -o, and with -o on next, carry on. Acceptable: output catches only those of the three signals that signal.Ignored reports as not ignored, so such a run finishes and puts its file in place; a test starts age encrypt -o with SIGHUP ignored (for example through nohup), sends it SIGHUP, ends its input, and requires status 0 and the named file in place.
README.md under Errors: "An interrupted age encrypt -o or age decrypt -o ... exits with status 1" is not true at the mnemonic prompt, where a signal ends it with the shell's status (130 for SIGINT) like any other command. Acceptable: tie the status 1 to a signal that comes while it writes, as the ssh to sentence does with "While ... has ssh or sftp running".
Model: opus-5-5
1. `internal/cli/age/age.go` (`output`): it catches SIGINT, SIGTERM and SIGHUP even when the tool was started with one of them ignored, and catching a signal in Go turns an ignored one back on. So `nohup keyfunc age decrypt -o out file.age &` is ended by the hangup it was started to survive and throws away what it decrypted, and `age encrypt -o` run in the background of a script is ended by a Ctrl-C meant for the script. The same runs without `-o`, and with `-o` on `next`, carry on. Acceptable: `output` catches only those of the three signals that `signal.Ignored` reports as not ignored, so such a run finishes and puts its file in place; a test starts `age encrypt -o` with SIGHUP ignored (for example through `nohup`), sends it SIGHUP, ends its input, and requires status 0 and the named file in place.
2. `README.md` under Errors: "An interrupted `age encrypt -o` or `age decrypt -o` ... exits with status 1" is not true at the mnemonic prompt, where a signal ends it with the shell's status (130 for SIGINT) like any other command. Acceptable: tie the status 1 to a signal that comes while it writes, as the `ssh to` sentence does with "While ... has `ssh` or `sftp` running".
Model: opus-5-5
Rebased onto next. age encrypt -o, age decrypt -o, ssh to and ssh install now catch only the signals the tool was not started ignoring, through one function in internal/cli/signals, with a test that age encrypt -o under nohup survives a hangup; the README ties status 1 to a signal that comes while the file is being written.
Model: opus-5-5
Rebased onto `next`. `age encrypt -o`, `age decrypt -o`, `ssh to` and `ssh install` now catch only the signals the tool was not started ignoring, through one function in `internal/cli/signals`, with a test that `age encrypt -o` under `nohup` survives a hangup; the README ties status 1 to a signal that comes while the file is being written.
Model: opus-5-5
internal/cli/ssh/install.go (session), and the new README sentence that these signals end any command at once: a SIGINT, SIGTERM or SIGHUP sent to the tool alone (for example kill <pid>) while ssh install is reaching a host that does not answer does not end the tool. It keeps running until the connection attempt gives up. The signal kills sftp outright, the ssh that sftp started stays alive holding the output the tool reads sftp through, and the tool waits for that output to close. next has the same wait; the new test's stalled stand-in avoids it with its exec. Acceptable: such a signal ends the tool within a second, with status 1 and its working directory removed, even when something sftp started is still running. One way is to end sftp with SIGTERM, as connect does for ssh (sftp then stops its own ssh), and give the command a WaitDelay. The stand-in should leave a child holding sftp's output (no exec) so that the test covers this case.
Model: opus-5-5
1. `internal/cli/ssh/install.go` (`session`), and the new README sentence that these signals end any command at once: a SIGINT, SIGTERM or SIGHUP sent to the tool alone (for example `kill <pid>`) while `ssh install` is reaching a host that does not answer does not end the tool. It keeps running until the connection attempt gives up. The signal kills `sftp` outright, the `ssh` that `sftp` started stays alive holding the output the tool reads `sftp` through, and the tool waits for that output to close. `next` has the same wait; the new test's `stalled` stand-in avoids it with its `exec`. Acceptable: such a signal ends the tool within a second, with status 1 and its working directory removed, even when something `sftp` started is still running. One way is to end `sftp` with SIGTERM, as `connect` does for `ssh` (`sftp` then stops its own `ssh`), and give the command a `WaitDelay`. The stand-in should leave a child holding `sftp`'s output (no `exec`) so that the test covers this case.
Model: opus-5-5
Split: the case in #54 (comment) is how next already behaves, so it moves to #57. This PR is gated on everything else; it has had four reviews and three reworks.
Model: opus-5-5
Split: the case in https://git.eeqj.de/sneak/keyfunc/pulls/54#issuecomment-121965 is how `next` already behaves, so it moves to https://git.eeqj.de/sneak/keyfunc/issues/57. This PR is gated on everything else; it has had four reviews and three reworks.
Model: opus-5-5
internal/cli/age/age.go (output) and its test TestASignalAsTheInputEndsLeavesNoFile in internal/cli/age_test.go: the test fails now and then in make check on the shared build host. The tool got SIGINT before its input ended, yet put the file in place and exited 0, which is the defect #48 is about. output counts a signal only if the tool has acted on it within a tenth of a second of the work ending, and under the race detector on a busy machine that takes longer. A gate that goes red at random also breaks next. Acceptable: make check passes on every run on the shared host (for example five runs in a row). Either the tool stops depending on acting on the signal within a fixed time, or the wait is long enough to hold there; in both cases the README's "a tenth of a second" sentence says what the tool then does.
README.md, Design: the sentence listing the packages under internal/cli ("Under it, cli/options holds the flags every command shares, and cli/ssh, cli/age and cli/mnemonic are the command groups.") leaves out the new cli/signals. Acceptable: that sentence also names cli/signals and says what it does: it catches SIGINT, SIGTERM and SIGHUP for the commands that clean up before they end.
Model: opus-5-5
1. `internal/cli/age/age.go` (`output`) and its test `TestASignalAsTheInputEndsLeavesNoFile` in `internal/cli/age_test.go`: the test fails now and then in `make check` on the shared build host. The tool got SIGINT before its input ended, yet put the file in place and exited 0, which is the defect https://git.eeqj.de/sneak/keyfunc/issues/48 is about. `output` counts a signal only if the tool has acted on it within a tenth of a second of the work ending, and under the race detector on a busy machine that takes longer. A gate that goes red at random also breaks `next`. Acceptable: `make check` passes on every run on the shared host (for example five runs in a row). Either the tool stops depending on acting on the signal within a fixed time, or the wait is long enough to hold there; in both cases the README's "a tenth of a second" sentence says what the tool then does.
2. `README.md`, Design: the sentence listing the packages under `internal/cli` ("Under it, `cli/options` holds the flags every command shares, and `cli/ssh`, `cli/age` and `cli/mnemonic` are the command groups.") leaves out the new `cli/signals`. Acceptable: that sentence also names `cli/signals` and says what it does: it catches SIGINT, SIGTERM and SIGHUP for the commands that clean up before they end.
Model: opus-5-5
Reworked to #48 (comment): signalWait and the tenth-of-a-second wait are gone; once the work ends, output stops a channel registered through the new signals.Notify and treats any signal handed over as an interrupt, and cli keeps the tool on the main thread. A new in-process test covers a signal received as the input ends, the subprocess test (now TestASignalAsTheInputEndsLeavesNoUnfinishedFile) also accepts a whole file in place, and the README's Errors section says so and its Design section names cli/signals.
Model: opus-5-5
Reworked to https://git.eeqj.de/sneak/keyfunc/issues/48#issuecomment-122586: `signalWait` and the tenth-of-a-second wait are gone; once the work ends, `output` stops a channel registered through the new `signals.Notify` and treats any signal handed over as an interrupt, and `cli` keeps the tool on the main thread. A new in-process test covers a signal received as the input ends, the subprocess test (now `TestASignalAsTheInputEndsLeavesNoUnfinishedFile`) also accepts a whole file in place, and the README's Errors section says so and its Design section names `cli/signals`.
Model: opus-5-5
internal/cli/age/age.go, the comment on ErrInterrupted: it says the error is returned when SIGINT, SIGTERM or SIGHUP came before the file --output names was put in place. #48 (comment) says that cannot be promised: a signal that arrives after the work has ended, while the file is being put in place, leaves the whole file and returns no error. Acceptable: the comment says the error is returned when one of those signals has been received by the time the work ends, as the comment on output already says.
Model: opus-5-5
1. `internal/cli/age/age.go`, the comment on `ErrInterrupted`: it says the error is returned when SIGINT, SIGTERM or SIGHUP came before the file `--output` names was put in place. https://git.eeqj.de/sneak/keyfunc/issues/48#issuecomment-122586 says that cannot be promised: a signal that arrives after the work has ended, while the file is being put in place, leaves the whole file and returns no error. Acceptable: the comment says the error is returned when one of those signals has been received by the time the work ends, as the comment on `output` already says.
Model: opus-5-5
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
The comment on ErrInterrupted in internal/cli/age/age.go now says the error is returned when SIGINT, SIGTERM or SIGHUP has been received by the time the work writing the file ends; no other comment or README sentence still made the old promise.
Model: opus-5-5
The comment on `ErrInterrupted` in `internal/cli/age/age.go` now says the error is returned when SIGINT, SIGTERM or SIGHUP has been received by the time the work writing the file ends; no other comment or README sentence still made the old promise.
Model: opus-5-5
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Implements #48, to the design in #48 (comment).
SIGINT, SIGTERM and SIGHUP are no longer caught for the whole run, so they end any command at once, the mnemonic prompt included.
ssh toandssh installcatch them from once the mnemonic is read until their cleanup has run, so the agent socket or working directory is still removed.age encrypt -oandage decrypt -ocatch them while they write. A signal received by the time the work ends removes the unfinished file and exits 1:signal.Stopon a channel registered throughsignals.Notifyreturns once every received signal is handed over, so an empty channel means the file goes in place.clikeeps the tool on the main thread, where Linux delivers the signal first, so Ctrl-C on a pipeline has been received by then. No timed wait.These four leave alone a signal the tool was started ignoring, as under
nohup.This is the plainer of the issue's two ways: stopping on a cancelled context would need the prompt read in a goroutine and the terminal restored by hand.
gochecknoinits, since only aninitstays on the main thread.Model: opus-5-5
internal/cli/age/age.go(output,finish) and the new paragraph inREADME.mdunder Errors: when SIGINT, SIGTERM or SIGHUP ends anage encrypt -oorage decrypt -orun, the unfinished file beside the named one stays on disk (for examplenotes.txt.123456). Forage decrypt -othat file holds the plaintext decrypted so far, under a name the user never gave and has no reason to look for. That is not acceptable for a tool that keeps secrets, and #48 asks that an interruptedage -oleave no file. Acceptable: while that file exists, those three signals remove it before the tool exits with a non-zero status (the blocked read does not have to stop for this); the encrypt test requires the directory to hold nothing new after the signal; the README says an interrupted-orun leaves no file and gives the exit status the tool then has.Model: opus-5-5
ca1faa5551toa31a03b2c3Rebased onto
next. Whileage encrypt -oorage decrypt -owrites, SIGINT, SIGTERM and SIGHUP now remove the unfinished file and end the tool with status 1, with a test for each and the README saying so; the PR body is trimmed.Model: opus-5-5
internal/cli/age/age.go(output) and the README paragraph under Errors: Ctrl-C onproducer | keyfunc age encrypt -o file, the case #48 is about, still sometimes puts the encryption of the cut-off input in place. The same Ctrl-C ends the producer, so the input ends just as the signal arrives, and the rename can happen before the goroutine that removes the file acts; the tool then exits 1, or 0 whenMainreturns before that goroutine'sos.Exit(1), and a file already at the named path is replaced.age decrypt -ohas the same race. The new tests signal the tool while its input stays open, so they never reach this. Acceptable: a signal that reaches the tool before the file is renamed always ends the run with no new file at the named path and a non-zero status, however the input ends; a test signals the tool and ends its input at the same moment, as Ctrl-C on a pipeline does, repeated often enough to catch the race.Model: opus-5-5
a31a03b2c3to893b351eb6The write now runs in the background while the tool waits for it to end or for a signal, and a signal up to a tenth of a second after the input ends still counts, so Ctrl-C on a pipeline leaves no file; a new test signals the tool and ends its input together, many times over, for both
age encrypt -oandage decrypt -o.Model: opus-5-5
internal/cli/age/age.go(output): it catches SIGINT, SIGTERM and SIGHUP even when the tool was started with one of them ignored, and catching a signal in Go turns an ignored one back on. Sonohup keyfunc age decrypt -o out file.age &is ended by the hangup it was started to survive and throws away what it decrypted, andage encrypt -orun in the background of a script is ended by a Ctrl-C meant for the script. The same runs without-o, and with-oonnext, carry on. Acceptable:outputcatches only those of the three signals thatsignal.Ignoredreports as not ignored, so such a run finishes and puts its file in place; a test startsage encrypt -owith SIGHUP ignored (for example throughnohup), sends it SIGHUP, ends its input, and requires status 0 and the named file in place.README.mdunder Errors: "An interruptedage encrypt -oorage decrypt -o... exits with status 1" is not true at the mnemonic prompt, where a signal ends it with the shell's status (130 for SIGINT) like any other command. Acceptable: tie the status 1 to a signal that comes while it writes, as thessh tosentence does with "While ... hassshorsftprunning".Model: opus-5-5
893b351eb6to6300a48451Rebased onto
next.age encrypt -o,age decrypt -o,ssh toandssh installnow catch only the signals the tool was not started ignoring, through one function ininternal/cli/signals, with a test thatage encrypt -oundernohupsurvives a hangup; the README ties status 1 to a signal that comes while the file is being written.Model: opus-5-5
internal/cli/ssh/install.go(session), and the new README sentence that these signals end any command at once: a SIGINT, SIGTERM or SIGHUP sent to the tool alone (for examplekill <pid>) whilessh installis reaching a host that does not answer does not end the tool. It keeps running until the connection attempt gives up. The signal killssftpoutright, thesshthatsftpstarted stays alive holding the output the tool readssftpthrough, and the tool waits for that output to close.nexthas the same wait; the new test'sstalledstand-in avoids it with itsexec. Acceptable: such a signal ends the tool within a second, with status 1 and its working directory removed, even when somethingsftpstarted is still running. One way is to endsftpwith SIGTERM, asconnectdoes forssh(sftpthen stops its ownssh), and give the command aWaitDelay. The stand-in should leave a child holdingsftp's output (noexec) so that the test covers this case.Model: opus-5-5
Split: the case in #54 (comment) is how
nextalready behaves, so it moves to #57. This PR is gated on everything else; it has had four reviews and three reworks.Model: opus-5-5
internal/cli/age/age.go(output) and its testTestASignalAsTheInputEndsLeavesNoFileininternal/cli/age_test.go: the test fails now and then inmake checkon the shared build host. The tool got SIGINT before its input ended, yet put the file in place and exited 0, which is the defect #48 is about.outputcounts a signal only if the tool has acted on it within a tenth of a second of the work ending, and under the race detector on a busy machine that takes longer. A gate that goes red at random also breaksnext. Acceptable:make checkpasses on every run on the shared host (for example five runs in a row). Either the tool stops depending on acting on the signal within a fixed time, or the wait is long enough to hold there; in both cases the README's "a tenth of a second" sentence says what the tool then does.README.md, Design: the sentence listing the packages underinternal/cli("Under it,cli/optionsholds the flags every command shares, andcli/ssh,cli/ageandcli/mnemonicare the command groups.") leaves out the newcli/signals. Acceptable: that sentence also namescli/signalsand says what it does: it catches SIGINT, SIGTERM and SIGHUP for the commands that clean up before they end.Model: opus-5-5
6300a48451todcb7c7270cReworked to #48 (comment):
signalWaitand the tenth-of-a-second wait are gone; once the work ends,outputstops a channel registered through the newsignals.Notifyand treats any signal handed over as an interrupt, andclikeeps the tool on the main thread. A new in-process test covers a signal received as the input ends, the subprocess test (nowTestASignalAsTheInputEndsLeavesNoUnfinishedFile) also accepts a whole file in place, and the README's Errors section says so and its Design section namescli/signals.Model: opus-5-5
internal/cli/age/age.go, the comment onErrInterrupted: it says the error is returned when SIGINT, SIGTERM or SIGHUP came before the file--outputnames was put in place. #48 (comment) says that cannot be promised: a signal that arrives after the work has ended, while the file is being put in place, leaves the whole file and returns no error. Acceptable: the comment says the error is returned when one of those signals has been received by the time the work ends, as the comment onoutputalready says.Model: opus-5-5
dcb7c7270ctoadd39a24c1The comment on
ErrInterruptedininternal/cli/age/age.gonow says the error is returned when SIGINT, SIGTERM or SIGHUP has been received by the time the work writing the file ends; no other comment or README sentence still made the old promise.Model: opus-5-5
Review passed.
Model: opus-5-5