fix: keep bounded event consume runs alive after stdin EOF (#1285)

This commit is contained in:
Emrys1105
2026-06-08 18:09:21 +08:00
committed by GitHub
parent ee5113f9d0
commit ec2ffebf47
5 changed files with 109 additions and 12 deletions

View File

@@ -64,8 +64,8 @@ Use 'event schema <EventKey>' for parameter details.`,
cmd.Flags().StringVar(&o.jqExpr, "jq", "", "JQ expression to filter output")
cmd.Flags().BoolVar(&o.quiet, "quiet", false, "Suppress informational messages on stderr")
cmd.Flags().StringVar(&o.outputDir, "output-dir", "", "Write each event as a file in this directory (relative paths only; absolute paths and ~ are rejected to prevent path traversal)")
cmd.Flags().IntVar(&o.maxEvents, "max-events", 0, "Exit after N successful emits (0 = unlimited). Multi-worker EventKeys may emit up to workers-1 past N before all workers stop.")
cmd.Flags().DurationVar(&o.timeout, "timeout", 0, "Exit after DURATION (e.g. 30s, 2m). 0 = no timeout. Timeout is a normal exit (code 0; stderr 'reason: timeout').")
cmd.Flags().IntVar(&o.maxEvents, "max-events", 0, "Exit after N successful emits (0 = unlimited). Multi-worker EventKeys may emit up to workers-1 past N before all workers stop. Bounded runs ignore stdin EOF.")
cmd.Flags().DurationVar(&o.timeout, "timeout", 0, "Exit after DURATION (e.g. 30s, 2m). 0 = no timeout. Timeout is a normal exit (code 0; stderr 'reason: timeout'). Bounded runs ignore stdin EOF.")
cmd.Flags().String("as", "auto", "identity type: user | bot | auto (must match EventKey's declared AuthTypes)")
_ = cmd.RegisterFlagCompletionFunc("as", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"user", "bot", "auto"}, cobra.ShellCompDirectiveNoFileComp
@@ -184,8 +184,9 @@ func runConsume(cmd *cobra.Command, f *cmdutil.Factory, eventKey string, o consu
errOut = io.Discard
}
// Non-TTY only: stdin EOF is shutdown for subprocess callers; in TTY Ctrl-D must not exit.
if !f.IOStreams.IsTerminal {
// Non-TTY unbounded consumers use stdin EOF as shutdown for subprocess callers.
// Bounded runs already have --max-events/--timeout as their lifecycle control.
if shouldWatchStdinEOF(f.IOStreams.IsTerminal, o.maxEvents, o.timeout) {
watchStdinEOF(os.Stdin, cancel, errOut)
}
@@ -370,3 +371,8 @@ func watchStdinEOF(r io.Reader, cancel context.CancelFunc, errOut io.Writer) {
cancel()
}()
}
// shouldWatchStdinEOF gates the stdin-EOF shutdown watcher: non-TTY unbounded runs only (<= 0 mirrors downstream's >0-is-bounded semantics, so negative bounds stay unbounded).
func shouldWatchStdinEOF(isTerminal bool, maxEvents int, timeout time.Duration) bool {
return !isTerminal && maxEvents <= 0 && timeout <= 0
}

View File

@@ -61,3 +61,70 @@ func TestWatchStdinEOF_DiagnosticMessage(t *testing.T) {
t.Fatal("watchStdinEOF did not cancel within 1s of EOF")
}
}
func TestShouldWatchStdinEOF(t *testing.T) {
tests := []struct {
name string
isTerminal bool
maxEvents int
timeout time.Duration
want bool
}{
{
name: "terminal",
isTerminal: true,
want: false,
},
{
name: "non terminal unbounded",
want: true,
},
{
name: "non terminal negative max events is unbounded",
maxEvents: -1,
want: true,
},
{
name: "non terminal negative timeout is unbounded",
timeout: -1 * time.Second,
want: true,
},
{
name: "non terminal max events bounded",
maxEvents: 1,
want: false,
},
{
name: "non terminal timeout bounded",
timeout: 10 * time.Minute,
want: false,
},
{
name: "non terminal both bounds positive",
maxEvents: 1,
timeout: 10 * time.Minute,
want: false,
},
{
name: "non terminal bounded max events with negative timeout",
maxEvents: 1,
timeout: -1 * time.Second,
want: false,
},
{
name: "non terminal bounded timeout with negative max events",
maxEvents: -1,
timeout: 10 * time.Minute,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := shouldWatchStdinEOF(tt.isTerminal, tt.maxEvents, tt.timeout)
if got != tt.want {
t.Fatalf("shouldWatchStdinEOF() = %v, want %v", got, tt.want)
}
})
}
}