Skip to content

Commit 07cbbcf

Browse files
committed
Stage: improve docstring
1 parent b30a165 commit 07cbbcf

1 file changed

Lines changed: 65 additions & 49 deletions

File tree

pipe/stage.go

Lines changed: 65 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,89 +7,105 @@ import (
77
// Stage is an element of a `Pipeline`. It reads from standard input
88
// and writes to standard output.
99
//
10-
// Who closes stdin and stdout?
10+
// # Who closes stdin and stdout?
1111
//
1212
// A `Stage` as a whole is responsible for closing its end of stdin
1313
// and stdout if the corresponding stream is closing. That
14-
// responsibility transfers to the stage as soon as `Start()` is called
15-
// and applies even if `Start()` returns an error. Before returning an
16-
// error from `Start()`, the stage must close any closing stream that
17-
// it has not already handed off to something else that will close it
18-
// promptly. The caller must not close a closing stream after passing it
19-
// to `Start()`.
14+
// responsibility transfers to the stage as soon as the stage's
15+
// `Start()` method is called and applies even if `Start()` returns an
16+
// error. Before returning an error from `Start()`, the stage must
17+
// close any closing stream that it has not already handed off to
18+
// something else that will close it promptly. The caller must not
19+
// close a closing stream after passing it to `Start()`.
2020
//
21-
// If the caller wants to retain ownership of stdin/stdout, it passes a
22-
// non-closing stream. The stage must not close a non-closing stream,
23-
// even if `Start()` returns an error.
21+
// If the caller wants to retain ownership of stdin/stdout, it passes
22+
// a non-closing stream. Calling `Close()` on a non-closing stream is
23+
// a NOP, so it isn't harmful but isn't required.
2424
//
25-
// Closing stdin/stdout tells the previous/next stage that this stage is
26-
// done reading/writing data, which can affect their behavior. Therefore,
27-
// after a successful start, a stage should close each one as soon as it
28-
// is done with it.
25+
// Closing stdin/stdout tells the previous/next stage that this stage
26+
// is done reading/writing data, which can affect their behavior.
27+
// Therefore, after a successful start, a stage should close each one
28+
// as soon as it is done with it. Assume that this is done via the
29+
// following local function
2930
//
30-
// How this should be done depends on whether stdin/stdout are of type
31-
// `*os.File`.
31+
// closeStreams := func() {
32+
// // Error handling omitted.
33+
// _ = stdin.Close()
34+
// _ = stdout.Close()
35+
// }
36+
//
37+
// From the point of view of the pipeline as a whole, if stdin is
38+
// provided by the user (`WithStdin()`), then we don't want the first
39+
// stage to close it at all, whether it's an `*os.File` or not. The
40+
// pipeline communicates this by passing a non-closing `InputStream`
41+
// when it starts that stage. For stdout, it depends on whether the
42+
// user supplied it using `WithStdout()` or `WithStdoutCloser()`. In
43+
// any case, this function can close the streams anyway, because
44+
// `InputStream.Close()` and `OutputStream.Close()` do nothing in
45+
// those cases.
46+
//
47+
// When these closes should happen depends on what kind of stage it is
48+
// and whether stdin/stdout are of type `*os.File`.
49+
//
50+
// ## A command stage
3251
//
3352
// If a stage is an external command, then the subprocess ultimately
3453
// needs its own copies of `*os.File` file descriptors for its stdin
3554
// and stdout. The external command will "always" [1] close those when
3655
// it exits.
3756
//
57+
// (It's theoretically possible for a command to pass the open file
58+
// descriptor to another, longer-lived process, in which case the file
59+
// descriptor wouldn't necessarily get closed when the command
60+
// finishes. But that's ill-behaved in a command that is being used in
61+
// a pipeline, so we'll ignore that possibility.)
62+
//
3863
// If the stage is an external command and one of the arguments is an
3964
// `*os.File`, then it can set the corresponding field of `exec.Cmd`
4065
// to that argument directly. This has the result that `exec.Cmd`
4166
// duplicates that file descriptor and passes the dup to the
4267
// subprocess. Therefore, the stage must close its copy of that
4368
// argument as soon as the external command has started, because the
4469
// external command will keep its own copy open as long as necessary
45-
// (and no longer!). It should use roughly the following sequence:
70+
// (and no longer!). Therefore, it should use roughly the following
71+
// sequence:
4672
//
47-
// cmd.Stdin = stdin.Reader() // Similarly for stdout
48-
// cmd.Start(…)
49-
// stdin.Close() // Close our copy
50-
// cmd.Wait()
73+
// cmd.Stdin = stdin.Reader()
74+
// cmd.Stdout = stdout.Writer()
75+
// err := cmd.Start(…)
76+
// // Close our copies as soon as the command has started:
77+
// closeStreams()
78+
// if err != nil {
79+
// return err
80+
// }
81+
// return cmd.Wait()
5182
//
5283
// If the stage is an external command and its stdin is not an
5384
// `*os.File`, then `exec.Cmd` will take care of creating an
5485
// `os.Pipe()`, copying from the provided reader into the pipe, and
5586
// eventually closing both ends of the pipe. The stage must close the
56-
// provided stdin itself, but only _after_ the external command has
57-
// finished, like so:
87+
// provided stdin itself, but only _after_ the external command and
88+
// the copy have finished, like so:
5889
//
59-
// cmd.Stdin = stdin.Reader() // Similarly for stdout
60-
// cmd.Start(…)
61-
// cmd.Wait()
62-
// stdin.Close() // Close
90+
// defer closeStreams()
91+
// cmd.Stdin = stdin.Reader()
92+
// cmd.Stdout = stdout.Writer()
93+
// err := cmd.Start(…)
94+
// if err != nil {
95+
// return err
96+
// }
97+
// return cmd.Wait()
6398
//
64-
// If the stage is an external command and its stdout is not an
65-
// `*os.File`, the stage creates a pipe, passes the write end to the
66-
// command, and copies from the read end to the provided writer. The
67-
// stage must close the provided stdout itself, but only _after_ the
68-
// external command and the copy have finished.
99+
// ## A function stage
69100
//
70101
// If the stage is a Go function, then it holds the only copy of
71102
// stdin/stdout, so it must wait until the function is done before
72103
// closing them (regardless of their underlying type, like so:
73104
//
74105
// go func() {
75-
// f(…, stdin, stdout)
76-
// stdin.Close()
77-
// stdout.Close()
106+
// defer closeStreams()
107+
// f(…, stdin.Reader(), stdout.Writer())
78108
// }()
79-
//
80-
// From the point of view of the pipeline as a whole, if stdin is
81-
// provided by the user (`WithStdin()`), then we don't want the first
82-
// stage to close it at all, whether it's an `*os.File` or not. The
83-
// pipeline communicates this by passing a non-closing `InputStream`
84-
// when it starts that stage. For stdout, it depends on whether the
85-
// user supplied it using `WithStdout()` or `WithStdoutCloser()`.
86-
//
87-
// [1] It's theoretically possible for a command to pass the open file
88-
// descriptor to another, longer-lived process, in which case the
89-
// file descriptor wouldn't necessarily get closed when the
90-
// command finishes. But that's ill-behaved in a command that is
91-
// being used in a pipeline, so we'll ignore that possibility.
92-
93109
type Stage interface {
94110
// Name returns the name of the stage.
95111
Name() string

0 commit comments

Comments
 (0)