Skip to content

Decouple close-responsibility from stdin/stdout dynamic type#53

Open
znull wants to merge 1 commit into
znull/stage-v2-cleanfrom
split/close-decouple
Open

Decouple close-responsibility from stdin/stdout dynamic type#53
znull wants to merge 1 commit into
znull/stage-v2-cleanfrom
split/close-decouple

Conversation

@znull
Copy link
Copy Markdown
Contributor

@znull znull commented May 31, 2026

Rather than inferring who is responsible for closing a pipeline stage's stdin/stdout from the dynamic type, communicate that explicitly in StartOptions.

Formerly, wrapping with a {reader,writer}NopCloser served two distinct purposes:

  • Mark whether the stream should be closed or not.
  • Recover the underlying concrete type being wrapped, to enable optimized fast
    paths that trigger based on dynamic type.

This PR fully moves the responsibility for "should we close this stream" to use StartOptions rather than type introspection.

We still want the wrapping for the latter purpose, though. WithStdin/WithStdout accept a bare io.Reader/io.Writer, while a stage's Start needs an io.ReadCloser/WriteCloser, so we have to adapt one to the other. The obvious thing, doing that with io.NopCloser, would hide the concrete type, meaning we can't use fast paths like passing an *os.File's fd into exec.Cmd or using io.ReaderFrom to trigger splice(2) instead of a userspace copy. Our {reader,writer}NopCloser hides the type the same way, but UnwrapReader/UnwrapWriter can peel it back off, so the information survives the round trip.

That's the same reason why v2 exports UnwrapReader and UnwrapWriter - although it has no current external callers, exporting them enables hypothetical user-written pipeline stages to opt into the same optimizations.

Why do all this, though, when Leave*Open are used if (and only if) wrapping? Because it makes the code in commandStage.Start easier to reason about. That area was the source of actual bugs/regressions during these refactorings.

/cc @mhagger

Rather than inferring who is responsible for closing a pipeline stage's
stdin/stdout from the dynamic type, communicate that explicitly in
StartOptions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@znull znull self-assigned this May 31, 2026
@znull znull requested a review from a team as a code owner May 31, 2026 14:02
Copilot AI review requested due to automatic review settings May 31, 2026 14:02
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR makes stdin/stdout close ownership explicit through StartOptions instead of relying on wrapper/dynamic types, aligning pipeline-stage startup with clearer lifecycle responsibility.

Changes:

  • Adds LeaveStdinOpen and LeaveStdoutOpen to StartOptions.
  • Propagates close-responsibility flags from Pipeline to stages.
  • Updates command/function stage close behavior and adds coverage for the new flags.
Show a summary per file
File Description
pipe/stage.go Documents explicit close-responsibility signaling via StartOptions.
pipe/pipeline.go Tracks stdout close ownership and passes leave-open flags to stages.
pipe/nop_closer.go Simplifies writer nop-closer documentation.
pipe/function.go Makes function stages honor leave-open flags before closing streams.
pipe/command.go Makes command stages use explicit leave-open options while preserving unwrap fast paths.
pipe/close_responsibility_test.go Adds tests for close behavior in function and command stages.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 6/6 changed files
  • Comments generated: 0

@mhagger
Copy link
Copy Markdown
Member

mhagger commented Jun 1, 2026

I only had time to glance at this so I'm shooting from the hip here…

From the title I was psyched about this PR. Not having to wrap and unwrap stdin and stdout would remove a lot of complexity. But reading the code, it looks like they still have to be wrapped and unwrapped. So now whether or not to close them is recorded redundantly, in two places: once by whether they are wrapped, and a second time in StartOptions. I'll read it more carefully later, with an eye to understanding why this counts as a simplification.

It would be cool if the the initial PR comment would address this.

@znull
Copy link
Copy Markdown
Contributor Author

znull commented Jun 1, 2026

It would be cool if the the initial PR comment would address this.

@mhagger I tried to expand the PR description to summarize it better. tl;dr I think this code reads more clearly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants