/*
 * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/*
 * @test
 * @bug 8289643 8291760
 * @requires (os.family == "linux" & !vm.musl)
 * @summary File descriptor leak detection with ProcessBuilder.startPipeline
 * @run testng/othervm PipelineLeaksFD
 */

@Test
public class PipelineLeaksFD {
    @DataProvider
    public Object[][] builders() {
        return new Object[][]{
                {List.of(new ProcessBuilder("cat"))},
                {List.of(new ProcessBuilder("cat"),
                        new ProcessBuilder("cat"))},
                {List.of(new ProcessBuilder("cat"),
                        new ProcessBuilder("cat"),
                        new ProcessBuilder("cat"),
                        new ProcessBuilder("cat"),
                        new ProcessBuilder("cat"))},
        };
    }

    @Test(dataProvider = "builders")
    void checkForLeaks(List<ProcessBuilder> builders) throws IOException {

        Set<PipeRecord> pipesBefore = myPipes();
        if (pipesBefore.size() < 3) {
            System.out.println(pipesBefore);
            Assert.fail("There should be at least 3 pipes before, (0, 1, 2)");
        }

        List<Process> processes = ProcessBuilder.startPipeline(builders);

        // Write something through the pipeline
        final String text = "xyz";
        try (Writer out = processes.get(0).outputWriter()) {
            out.write(text);
        }

        // Read, check, and close all streams
        for (int i = 0; i < processes.size(); i++) {
            final Process p = processes.get(i);
            String expectedOut = (i == processes.size() - 1) ? text : null;
            String expectedErr = null;      // EOF
            try (BufferedReader inputStream = p.inputReader();
                 BufferedReader errorStream = p.errorReader()) {
                String outActual = inputStream.readLine();
                Assert.assertEquals(outActual, expectedOut, "stdout, process[ " + i + "]: " + p);

                String errActual = errorStream.readLine();
                Assert.assertEquals(errActual, expectedErr, "stderr, process[ " + i + "]: " + p);
            }
        }

        processes.forEach(p -> waitForQuiet(p));

        Set<PipeRecord> pipesAfter = myPipes();
        if (!pipesBefore.equals(pipesAfter)) {
            Set<PipeRecord> missing = new HashSet<>(pipesBefore);
            missing.removeAll(pipesAfter);
            printPipes(missing, "Missing from pipesAfter");
            Set<PipeRecord> extra = new HashSet<>(pipesAfter);
            extra.removeAll(pipesBefore);
            printPipes(extra, "Extra pipes in pipesAfter");
            Assert.fail("More or fewer pipes than expected");
        }
    }

    static void printPipes(Set<PipeRecord> pipes, String label) {
        System.out.printf("%s: [%d]%n", label, pipes.size());
        pipes.forEach(r -> System.out.printf("%-20s: %s%n", r.fd(), r.link()));
    }

    static void waitForQuiet(Process p) {
        try {
            int st = p.waitFor();
            if (st != 0) {
                System.out.println("non-zero exit status: " + p);
            }
        } catch (InterruptedException ie) {
        }
    }

    /**
     * Collect a Set of pairs of /proc fd paths and the symbol links that are pipes.
     * @return A set of PipeRecords, possibly empty
     */
    static Set<PipeRecord> myPipes() {
        Path path = Path.of("/proc/" + ProcessHandle.current().pid() + "/fd");
        Set<PipeRecord> pipes = new HashSet<>();
        File[] files = path.toFile().listFiles(f -> Files.isSymbolicLink(f.toPath()));
        if (files != null) {
            for (File file : files) {
                try {
                    Path link = Files.readSymbolicLink(file.toPath());
                    if (link.toString().startsWith("pipe:")) {
                        pipes.add(new PipeRecord(file.toPath(), link));
                    }
                } catch (IOException ioe) {
                }
            }
        }
        return pipes;
    }

    record PipeRecord(Path fd, Path link) { };
}