Cancel actions on process interrupts

This commit is contained in:
2026-08-13 00:24:16 +00:00
parent 1d3ea64541
commit 4d5a1d9709
5 changed files with 78 additions and 1 deletions

View File

@@ -3,14 +3,23 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"os" "os"
"os/signal"
"syscall"
"gitea.maximumdirect.net/eric/weatherreporter/internal/cli" "gitea.maximumdirect.net/eric/weatherreporter/internal/cli"
) )
func main() { func main() {
if err := cli.Run(context.Background(), os.Args[1:], os.Stdout, os.Stderr); err != nil { if err := runCommand(os.Args[1:], os.Stdout, os.Stderr, cli.Run); err != nil {
fmt.Fprintf(os.Stderr, "weatherreporter: %v\n", err) fmt.Fprintf(os.Stderr, "weatherreporter: %v\n", err)
os.Exit(1) os.Exit(1)
} }
} }
func runCommand(args []string, stdout, stderr io.Writer, runner func(context.Context, []string, io.Writer, io.Writer) error) error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
return runner(ctx, args, stdout, stderr)
}

View File

@@ -0,0 +1,56 @@
package main
import (
"context"
"errors"
"io"
"os"
"syscall"
"testing"
"time"
)
func TestRunCommandCancelsActionContextOnSignal(t *testing.T) {
for _, tt := range []struct {
name string
signal os.Signal
}{
{name: "Interrupt", signal: os.Interrupt},
{name: "Terminate", signal: syscall.SIGTERM},
} {
t.Run(tt.name, func(t *testing.T) {
started := make(chan struct{})
done := make(chan error, 1)
go func() {
done <- runCommand(nil, io.Discard, io.Discard, func(ctx context.Context, _ []string, _, _ io.Writer) error {
close(started)
<-ctx.Done()
return ctx.Err()
})
}()
select {
case <-started:
case <-time.After(time.Second):
t.Fatal("runner did not receive an action context")
}
process, err := os.FindProcess(os.Getpid())
if err != nil {
t.Fatalf("FindProcess() error = %v", err)
}
if err := process.Signal(tt.signal); err != nil {
t.Fatalf("Signal(%v) error = %v", tt.signal, err)
}
select {
case err := <-done:
if !errors.Is(err, context.Canceled) {
t.Fatalf("runCommand() error = %v, want context cancellation", err)
}
case <-time.After(time.Second):
t.Fatal("interrupt did not cancel the action context")
}
})
}
}

View File

@@ -68,6 +68,10 @@ failure before publication leaves an existing destination unchanged. A
notification failure occurs after publication, so the newly written output notification failure occurs after publication, so the newly written output
remains available. remains available.
`SIGINT` and `SIGTERM` cancel an active action. Weatherreporter lets that
cancellation reach the action before exiting; when the action has a result, it
emits the usual failed summary and exits nonzero.
Action commands (`generate`, `run`, and `compare`) write a JSON summary to Action commands (`generate`, `run`, and `compare`) write a JSON summary to
stdout unless `--quiet` is set. `run` also writes compact per-report and batch stdout unless `--quiet` is set. `run` also writes compact per-report and batch
status lines to stderr. A pre-run error, such as an invalid flag, missing status lines to stderr. A pre-run error, such as an invalid flag, missing

View File

@@ -4,6 +4,10 @@
The root `--version` flag reports the build version supplied by `internal/buildinfo`. Tagged release builds replace its development default at link time. The root `--version` flag reports the build version supplied by `internal/buildinfo`. Tagged release builds replace its development default at link time.
The executable derives its action context from `SIGINT` and `SIGTERM` and
passes it to `Runner.Run`. Signal cancellation therefore uses the same action,
summary, and error paths as other context cancellation.
For each `generate` or `run` action, `Runner` constructs one project-owned Promptkit executor after configuration loads. It captures an absolute working directory, resolves only a relative explicit output override against it, and passes the working directory, loaded configuration, resolved override, and any `--llm-debug-dir` request to the app. The raw configured fallback remains in the configuration for app-owned destination selection. `run` uses the same explicit-resolution rule for `--out-dir`. For each `generate` or `run` action, `Runner` constructs one project-owned Promptkit executor after configuration loads. It captures an absolute working directory, resolves only a relative explicit output override against it, and passes the working directory, loaded configuration, resolved override, and any `--llm-debug-dir` request to the app. The raw configured fallback remains in the configuration for app-owned destination selection. `run` uses the same explicit-resolution rule for `--out-dir`.
The CLI dispatches only generation and batch actions. It has no persisted-run or inspection dispatch. Summaries include report identity, status, output path, effective profile/backend/model, source warnings, validation, requested debug path, and notification result when available. They intentionally exclude prompt input, raw generated text, render context, endpoints, credentials, and full Distributor payloads. A failed action with a partial result still emits its safe summary before its error is returned. The CLI dispatches only generation and batch actions. It has no persisted-run or inspection dispatch. Summaries include report identity, status, output path, effective profile/backend/model, source warnings, validation, requested debug path, and notification result when available. They intentionally exclude prompt input, raw generated text, render context, endpoints, credentials, and full Distributor payloads. A failed action with a partial result still emits its safe summary before its error is returned.

View File

@@ -36,6 +36,10 @@ absolute output path and active profile, backend, model, warnings, validation,
debug, and notification information; see the [CLI reference](cli.md) for its debug, and notification information; see the [CLI reference](cli.md) for its
exact fields. exact fields.
`SIGINT` and `SIGTERM` request orderly cancellation of an active action. The
command lets cancellation and related cleanup finish before it exits; use the
usual failed result or error to determine whether an output was published.
## Batch Outputs And Distributor Notification ## Batch Outputs And Distributor Notification
Run a scheduled batch with an explicit output directory when appropriate: Run a scheduled batch with an explicit output directory when appropriate: