diff --git a/src/runtime/containerd-shim-v2/stream_test.go b/src/runtime/containerd-shim-v2/stream_test.go index 637a20b034..d944c81fc2 100644 --- a/src/runtime/containerd-shim-v2/stream_test.go +++ b/src/runtime/containerd-shim-v2/stream_test.go @@ -89,3 +89,189 @@ func TestNewTtyIOFifoReopen(t *testing.T) { checkFifoRead(outr) checkFifoRead(errr) } + +func TestIoCopy(t *testing.T) { + assert := assert.New(t) + ctx := context.TODO() + + testBytes1 := []byte("Test1") + testBytes2 := []byte("Test2") + testBytes3 := []byte("Test3") + + fifoPath, err := ioutil.TempDir(testDir, "fifo-path-") + assert.NoError(err) + dstStdoutPath := filepath.Join(fifoPath, "dststdout") + dstStderrPath := filepath.Join(fifoPath, "dststderr") + + // test function: create pipes, and use ioCopy() to copy data from one set to the other + // this function will be called multiple times, testing different combinations of closing order + // in order to verify that closing a pipe doesn't break the copy for the others + ioCopyTest := func(first, second, third string) { + var srcStdinPath string + if third != "" { + srcStdinPath = filepath.Join(fifoPath, "srcstdin") + } + + logErrorMsg := func(msg string) string { + return "Error found while using order [" + first + " " + second + " " + third + "] - " + msg + } + + exitioch := make(chan struct{}) + stdinCloser := make(chan struct{}) + + createFifo := func(f string) (io.ReadCloser, io.WriteCloser) { + reader, err := fifo.OpenFifo(ctx, f, syscall.O_RDONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0700) + if err != nil { + t.Fatal(err) + } + writer, err := fifo.OpenFifo(ctx, f, syscall.O_WRONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0700) + if err != nil { + reader.Close() + t.Fatal(err) + } + return reader, writer + } + + // create two sets of stdin, stdout and stderr pipes, to copy data from one to the other + srcOutR, srcOutW := createFifo(filepath.Join(fifoPath, "srcstdout")) + defer srcOutR.Close() + defer srcOutW.Close() + + srcErrR, srcErrW := createFifo(filepath.Join(fifoPath, "srcstderr")) + defer srcErrR.Close() + defer srcErrW.Close() + + dstInR, dstInW := createFifo(filepath.Join(fifoPath, "dststdin")) + defer dstInR.Close() + defer dstInW.Close() + + dstOutR, err := fifo.OpenFifo(ctx, dstStdoutPath, syscall.O_RDONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0700) + if err != nil { + t.Fatal(err) + } + defer dstOutR.Close() + dstErrR, err := fifo.OpenFifo(ctx, dstStderrPath, syscall.O_RDONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0700) + if err != nil { + t.Fatal(err) + } + defer dstErrR.Close() + + var srcInW io.WriteCloser + if srcStdinPath != "" { + srcInW, err = fifo.OpenFifo(ctx, srcStdinPath, syscall.O_WRONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0700) + if err != nil { + t.Fatal(err) + } + defer srcInW.Close() + } + + tty, err := newTtyIO(ctx, srcStdinPath, dstStdoutPath, dstStderrPath, false) + assert.NoError(err) + defer tty.close() + + // start the ioCopy threads : copy from src to dst + go ioCopy(exitioch, stdinCloser, tty, dstInW, srcOutR, srcErrR) + + var firstW, secondW, thirdW io.WriteCloser + var firstR, secondR, thirdR io.Reader + getPipes := func(order string) (io.Reader, io.WriteCloser) { + switch order { + case "out": + return dstOutR, srcOutW + case "err": + return dstErrR, srcErrW + case "in": + return dstInR, srcInW + case "": + return nil, nil + } + t.Fatal("internal error") + return nil, nil + } + + firstR, firstW = getPipes(first) + secondR, secondW = getPipes(second) + thirdR, thirdW = getPipes(third) + + checkFifoWrite := func(w io.Writer, b []byte, name string) { + _, err := w.Write(b) + if name == "in" && (name == third || name == second && first == "out") { + // this is expected: when stdout is closed, ioCopy() will close stdin + // so if "in" is after "out", we will get here + } else { + assert.NoError(err, logErrorMsg("Write error on std"+name)) + } + } + checkFifoRead := func(r io.Reader, b []byte, name string) { + var err error + buf := make([]byte, 5) + done := make(chan struct{}) + timer := time.NewTimer(2 * time.Second) + go func() { + _, err = r.Read(buf) + close(done) + }() + select { + case <-done: + assert.NoError(err, logErrorMsg("Error reading from std"+name)) + assert.Equal(b, buf, logErrorMsg("Value mismatch on std"+name)) + case <-timer.C: + //t.Fatal(logErrorMsg("read fifo timeout on std" + name)) + if name == "in" && (name == third || name == second && first == "out") { + // this is expected: when stdout is closed, ioCopy() will close stdin + // so if "in" is after "out", we will get here + } else { + assert.Fail(logErrorMsg("read fifo timeout on std" + name)) + } + return + } + } + + // write to each pipe, and close them immediately + // the ioCopy function should copy the data, then stop the corresponding thread + checkFifoWrite(firstW, testBytes1, first) + firstW.Close() + + // need to make sure the Close() above is done before we continue + time.Sleep(time.Second) + + checkFifoWrite(secondW, testBytes2, second) + secondW.Close() + + if thirdW != nil { + // need to make sure the Close() above is done before we continue + time.Sleep(time.Second) + + checkFifoWrite(thirdW, testBytes3, third) + thirdW.Close() + } + + // wait for the end of the ioCopy + timer := time.NewTimer(2 * time.Second) + select { + case <-exitioch: + // now check that all data has been copied properly + checkFifoRead(firstR, testBytes1, first) + checkFifoRead(secondR, testBytes2, second) + if thirdR != nil { + checkFifoRead(thirdR, testBytes3, third) + } + case <-timer.C: + t.Fatal(logErrorMsg("timeout waiting for ioCopy()")) + } + } + + // try the different combinations + + // tests without stdin + ioCopyTest("out", "err", "") + ioCopyTest("err", "out", "") + + // tests with stdin + ioCopyTest("out", "err", "in") + ioCopyTest("out", "in", "err") + ioCopyTest("err", "out", "in") + ioCopyTest("err", "in", "out") + ioCopyTest("in", "out", "err") + ioCopyTest("in", "err", "out") +}