fix: allow attaching to any combination of stdin/stdout/stderr

Before this PR, if a stdin/stdout/stderr stream is nil,
and the corresponding FIFO is not an empty string,
a panic will occur when Read/Write of the nil stream is invoked in io.CopyBuffer.

Signed-off-by: Hsing-Yu (David) Chen <davidhsingyuchen@gmail.com>
This commit is contained in:
Hsing-Yu (David) Chen 2023-03-28 17:13:28 -07:00
parent 40f26543bd
commit 687a5f51a8
2 changed files with 106 additions and 39 deletions

View File

@ -166,6 +166,15 @@ func NewAttach(opts ...Opt) Attach {
if fifos == nil { if fifos == nil {
return nil, fmt.Errorf("cannot attach, missing fifos") return nil, fmt.Errorf("cannot attach, missing fifos")
} }
if streams.Stdin == nil {
fifos.Stdin = ""
}
if streams.Stdout == nil {
fifos.Stdout = ""
}
if streams.Stderr == nil {
fifos.Stderr = ""
}
return copyIO(fifos, streams) return copyIO(fifos, streams)
} }
} }

View File

@ -138,19 +138,62 @@ func TestNewFIFOSetInDir(t *testing.T) {
} }
func TestNewAttach(t *testing.T) { func TestNewAttach(t *testing.T) {
testCases := []struct {
name string
expectedStdin, expectedStdout, expectedStderr string
}{
{
name: "attach to all streams (stdin, stdout, and stderr)",
expectedStdin: "this is the stdin",
expectedStdout: "this is the stdout",
expectedStderr: "this is the stderr",
},
{
name: "don't attach to stdin",
expectedStdout: "this is the stdout",
expectedStderr: "this is the stderr",
},
{
name: "don't attach to stdout",
expectedStdin: "this is the stdin",
expectedStderr: "this is the stderr",
},
{
name: "don't attach to stderr",
expectedStdin: "this is the stdin",
expectedStdout: "this is the stdout",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var ( var (
expectedStdin = "this is the stdin" stdin = bytes.NewBufferString(tc.expectedStdin)
expectedStdout = "this is the stdout"
expectedStderr = "this is the stderr"
stdin = bytes.NewBufferString(expectedStdin)
stdout = new(bytes.Buffer) stdout = new(bytes.Buffer)
stderr = new(bytes.Buffer) stderr = new(bytes.Buffer)
)
withBytesBuffers := func(streams *Streams) { // The variables below have to be of the interface type (i.e., io.Reader/io.Writer)
*streams = Streams{Stdin: stdin, Stdout: stdout, Stderr: stderr} // instead of the concrete type (i.e., *bytes.Buffer) *before* being passed to NewAttach.
// Otherwise, in NewAttach, the interface value won't be nil
// (it's just that the concrete value inside the interface itself is nil. [1]),
// which means that the corresponding FIFO path won't be set to be an empty string,
// and that's not what we want.
//
// [1] https://go.dev/tour/methods/12
stdinArg io.Reader
stdoutArg, stderrArg io.Writer
)
if tc.expectedStdin != "" {
stdinArg = stdin
} }
attacher := NewAttach(withBytesBuffers) if tc.expectedStdout != "" {
stdoutArg = stdout
}
if tc.expectedStderr != "" {
stderrArg = stderr
}
attacher := NewAttach(WithStreams(stdinArg, stdoutArg, stderrArg))
fifos, err := NewFIFOSetInDir("", "theid", false) fifos, err := NewFIFOSetInDir("", "theid", false)
assert.NoError(t, err) assert.NoError(t, err)
@ -160,18 +203,23 @@ func TestNewAttach(t *testing.T) {
defer attachedFifos.Close() defer attachedFifos.Close()
producers := setupFIFOProducers(t, attachedFifos.Config()) producers := setupFIFOProducers(t, attachedFifos.Config())
initProducers(t, producers, expectedStdout, expectedStderr) initProducers(t, producers, tc.expectedStdout, tc.expectedStderr)
actualStdin, err := io.ReadAll(producers.Stdin) var actualStdin []byte
if producers.Stdin != nil {
actualStdin, err = io.ReadAll(producers.Stdin)
assert.NoError(t, err) assert.NoError(t, err)
}
attachedFifos.Wait() attachedFifos.Wait()
attachedFifos.Cancel() attachedFifos.Cancel()
assert.Nil(t, attachedFifos.Close()) assert.Nil(t, attachedFifos.Close())
assert.Equal(t, expectedStdout, stdout.String()) assert.Equal(t, tc.expectedStdout, stdout.String())
assert.Equal(t, expectedStderr, stderr.String()) assert.Equal(t, tc.expectedStderr, stderr.String())
assert.Equal(t, expectedStdin, string(actualStdin)) assert.Equal(t, tc.expectedStdin, string(actualStdin))
})
}
} }
type producers struct { type producers struct {
@ -187,26 +235,36 @@ func setupFIFOProducers(t *testing.T, fifos Config) producers {
ctx = context.Background() ctx = context.Background()
) )
if fifos.Stdin != "" {
pipes.Stdin, err = fifo.OpenFifo(ctx, fifos.Stdin, syscall.O_RDONLY, 0) pipes.Stdin, err = fifo.OpenFifo(ctx, fifos.Stdin, syscall.O_RDONLY, 0)
assert.NoError(t, err) assert.NoError(t, err)
}
if fifos.Stdout != "" {
pipes.Stdout, err = fifo.OpenFifo(ctx, fifos.Stdout, syscall.O_WRONLY, 0) pipes.Stdout, err = fifo.OpenFifo(ctx, fifos.Stdout, syscall.O_WRONLY, 0)
assert.NoError(t, err) assert.NoError(t, err)
}
if fifos.Stderr != "" {
pipes.Stderr, err = fifo.OpenFifo(ctx, fifos.Stderr, syscall.O_WRONLY, 0) pipes.Stderr, err = fifo.OpenFifo(ctx, fifos.Stderr, syscall.O_WRONLY, 0)
assert.NoError(t, err) assert.NoError(t, err)
}
return pipes return pipes
} }
func initProducers(t *testing.T, producers producers, stdout, stderr string) { func initProducers(t *testing.T, producers producers, stdout, stderr string) {
if producers.Stdout != nil {
_, err := producers.Stdout.Write([]byte(stdout)) _, err := producers.Stdout.Write([]byte(stdout))
assert.NoError(t, err) assert.NoError(t, err)
assert.Nil(t, producers.Stdout.Close()) assert.Nil(t, producers.Stdout.Close())
}
_, err = producers.Stderr.Write([]byte(stderr)) if producers.Stderr != nil {
_, err := producers.Stderr.Write([]byte(stderr))
assert.NoError(t, err) assert.NoError(t, err)
assert.Nil(t, producers.Stderr.Close()) assert.Nil(t, producers.Stderr.Close())
}
} }
func TestLogURIGenerator(t *testing.T) { func TestLogURIGenerator(t *testing.T) {