/*
 * Copyright (c) 1998, 2017, 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 java.io.*;
import java.rmi.*;
import java.rmi.activation.*;
import java.rmi.registry.*;
import java.time.LocalTime;
import java.util.concurrent.TimeoutException;

/**
 * Utility class that creates an instance of rmid with a policy
 * file of name <code>TestParams.defaultPolicy</code>.
 *
 * Activation groups should run with the same security manager as the
 * test.
 */
public class RMID extends JavaVM {

    // TODO: adjust these based on the timeout factor
    // such as jcov.sleep.multiplier; see start(long) method.
    // Also consider the test.timeout.factor property (a float).
    private static final long TIMEOUT_SHUTDOWN_MS = 60_000L;
    private static final long TIMEOUT_DESTROY_MS  = 10_000L;
    private static final long STARTTIME_MS        = 15_000L;
    private static final long POLLTIME_MS         = 100L;

    // when restart rmid, it may take more time than usual because of
    // "port in use" by a possible interloper (check JDK-8168975),
    // so need to set a longer timeout for restart.
    private static long restartTimeout;
    // Same reason to inheritedChannel in RMIDSelectorProvider.
    // Put it here rather than in RMIDSelectorProvider to adjust
    // both timeout values together.
    private static long inheritedChannelTimeout;

    private static final String SYSTEM_NAME = ActivationSystem.class.getName();
        // "java.rmi.activation.ActivationSystem"

    public static String MANAGER_OPTION="-Djava.security.manager=";

    /**
     * Test port for rmid.
     *
     * May initially be 0, which means that the child rmid process will choose
     * an ephemeral port and report it back to the parent process. This field
     * will then be set to the child rmid's ephemeral port value.
     */
    private volatile int port;
    //private final boolean ephemeralPort

    /** Initial log name */
    protected static String log = "log";
    /** rmid's logfile directory; currently must be "." */
    protected static String LOGDIR = ".";

    /** The output message from the child rmid process that directly precedes
     * the ephemeral port number.*/
    public static final String EPHEMERAL_MSG = "RmidSelectorProvider-listening-On:";

    private static void mesg(Object mesg) {
        System.err.println("RMID: " + mesg.toString());
    }

    /** make test options and arguments */
    private static String makeOptions(int port, boolean debugExec,
                                      boolean enableSelectorProvider) {

        String options = " -Dsun.rmi.server.activation.debugExec=" +
            debugExec;
        // +
        //" -Djava.compiler= ";

        // if test params set, want to propagate them
        if (!TestParams.testSrc.equals("")) {
            options += " -Dtest.src=" + TestParams.testSrc + " ";
        }
        //if (!TestParams.testClasses.equals("")) {
        //    options += " -Dtest.classes=" + TestParams.testClasses + " ";
        //}
        options += " -Dtest.classes=" + TestParams.testClasses //;
         +
         " -Djava.rmi.server.logLevel=v ";

        // +
        // " -Djava.security.debug=all ";

        // Set execTimeout to 60 sec (default is 30 sec)
        // to avoid spurious timeouts on slow machines.
        options += " -Dsun.rmi.activation.execTimeout=60000";

        // It's important to set handshakeTimeout to small value, for example
        // 5 sec (default is 60 sec) to avoid wasting too much time when
        // calling lookupSystem(port) in restart(), because
        //   1. If use default value of this option, it will take about 2 minutes
        //     to finish lookupSystem(port) in 2 loops in restart();
        //   2. If set this option as 5 sec then lookupSystem(port) will return
        //     very quickly.
        options += " -Dsun.rmi.transport.tcp.handshakeTimeout=5000";

        if (port == 0 || enableSelectorProvider) {
            // Ephemeral port, so have the rmid child process create the
            // server socket channel and report its port number, over stdin.
            options += " -classpath " + TestParams.testClassPath;
            options += " --add-exports=java.base/sun.nio.ch=ALL-UNNAMED";
            options += " -Djava.nio.channels.spi.SelectorProvider=RMIDSelectorProvider";
            options += " -Dtest.java.rmi.testlibrary.RMIDSelectorProvider.port=" + port;
            options += " -Dtest.java.rmi.testlibrary.RMIDSelectorProvider.timeout="
                        + inheritedChannelTimeout;

            // Disable redirection of System.err to /tmp
            options += " -Dsun.rmi.server.activation.disableErrRedirect=true";
        }

        return options;
    }

    private static String makeArgs() {
        return makeArgs(false, 0);
    }

    private static String makeArgs(boolean includePortArg, int port) {
        // getAbsolutePath requires permission to read user.dir
        String args =
            " -log " + (new File(LOGDIR, log)).getAbsolutePath();

        // 0 = ephemeral port, do not include an explicit port number
        if (includePortArg && port != 0) {
            args += " -port " + port;
        }

        // +
        //      " -C-Djava.compiler= ";

        // if test params set, want to propagate them
        if (!TestParams.testSrc.equals("")) {
            args += " -C-Dtest.src=" + TestParams.testSrc;
        }
        if (!TestParams.testClasses.equals("")) {
            args += " -C-Dtest.classes=" + TestParams.testClasses;
        }

        if (!TestParams.testJavaOpts.equals("")) {
            for (String a : TestParams.testJavaOpts.split(" +")) {
                args += " -C" + a;
            }
        }

        if (!TestParams.testVmOpts.equals("")) {
            for (String a : TestParams.testVmOpts.split(" +")) {
                args += " -C" + a;
            }
        }

        args += " -C-Djava.rmi.server.useCodebaseOnly=false ";

        args += " " + getCodeCoverageArgs();
        return args;
    }

    /**
     * Routine that creates an rmid that will run with or without a
     * policy file.
     */
    public static RMID createRMID() {
        return createRMID(System.out, System.err, true, true,
                          TestLibrary.getUnusedRandomPort());
    }

    public static RMID createRMID(OutputStream out, OutputStream err,
                                  boolean debugExec)
    {
        return createRMID(out, err, debugExec, true,
                          TestLibrary.getUnusedRandomPort());
    }

    public static RMID createRMID(OutputStream out, OutputStream err,
                                  boolean debugExec, boolean includePortArg,
                                  int port)
    {
        return createRMIDWithOptions(out, err, debugExec, includePortArg, port, "");
    }

    /**
     * Create a RMID on a specified port capturing stdout and stderr
     * with additional command line options and whether to print out
     * debugging information that is used for spawning activation groups.
     *
     * @param out the OutputStream where the normal output of the
     *            rmid subprocess goes
     * @param err the OutputStream where the error output of the
     *            rmid subprocess goes
     * @param debugExec whether to print out debugging information
     * @param includePortArg whether to include port argument
     * @param port the port on which rmid accepts requests
     * @param additionalOptions additional command line options
     * @return a RMID instance
     */
    public static RMID createRMIDWithOptions(OutputStream out, OutputStream err,
                                  boolean debugExec, boolean includePortArg,
                                  int port, String additionalOptions)
    {
        String options = makeOptions(port, debugExec, false);
        options += " " + additionalOptions;
        String args = makeArgs(includePortArg, port);
        RMID rmid = new RMID("sun.rmi.server.Activation", options, args,
                             out, err, port);
        rmid.setPolicyFile(TestParams.defaultRmidPolicy);

        return rmid;
    }

    public static RMID createRMIDOnEphemeralPort() {
        return createRMID(System.out, System.err, true, false, 0);
    }

    /**
     * Create a RMID on an ephemeral port capturing stdout and stderr
     * with additional command line options.
     *
     * @param additionalOptions additional command line options
     * @return a RMID instance
     */
    public static RMID createRMIDOnEphemeralPortWithOptions(
                                            String additionalOptions) {
        return createRMIDWithOptions(System.out, System.err,
                                     true, false, 0, additionalOptions);
    }

    public static RMID createRMIDOnEphemeralPort(OutputStream out,
                                                 OutputStream err,
                                                 boolean debugExec)
    {
        return createRMID(out, err, debugExec, false, 0);
    }


    /**
     * Private constructor. RMID instances should be created
     * using the static factory methods.
     */
    private RMID(String classname, String options, String args,
                   OutputStream out, OutputStream err, int port)
    {
        super(classname, options, args, out, err);
        this.port = port;
        long waitTime = (long)(240_000 * TestLibrary.getTimeoutFactor());
        restartTimeout = (long)(waitTime * 0.9);
        inheritedChannelTimeout = (long)(waitTime * 0.8);
    }

    /**
     * Removes rmid's log file directory.
     */
    public static void removeLog() {
        File f = new File(LOGDIR, log);

        if (f.exists()) {
            mesg("Removing rmid's old log file.");
            String[] files = f.list();

            if (files != null) {
                for (int i=0; i<files.length; i++) {
                    (new File(f, files[i])).delete();
                }
            }

            if (! f.delete()) {
                mesg("Warning: unable to delete old log file.");
            }
        }
    }

    /**
     * This method is used for adding arguments to rmid (not its VM)
     * for passing as VM options to its child group VMs.
     * Returns the extra command line arguments required
     * to turn on jcov code coverage analysis for rmid child VMs.
     */
    protected static String getCodeCoverageArgs() {
        return TestLibrary.getExtraProperty("rmid.jcov.args","");
    }

    /**
     * Looks up the activation system in the registry on the given port,
     * returning its stub, or null if it's not present. This method differs from
     * ActivationGroup.getSystem() because this method looks on a specific port
     * instead of using the java.rmi.activation.port property like
     * ActivationGroup.getSystem() does. This method also returns null instead
     * of throwing exceptions.
     */
    public static ActivationSystem lookupSystem(int port) {
        try {
            return (ActivationSystem)LocateRegistry.getRegistry(port).lookup(SYSTEM_NAME);
        } catch (RemoteException | NotBoundException ex) {
            return null;
        }
    }

    /**
     * Starts rmid and waits up to the default timeout period
     * to confirm that it's running.
     */
    public void start() throws IOException {
        start(STARTTIME_MS);
    }

    /**
     * Starts rmid and waits up to the given timeout period
     * to confirm that it's running.
     */
    public void start(long waitTime) throws IOException {

        // if rmid is already running, then the test will fail with
        // a well recognized exception (port already in use...).

        mesg("Starting rmid on port " + port + ", at " + LocalTime.now());
        int p = super.startAndGetPort();
        if (p != -1)
            port = p;
        mesg("Started rmid on port " + port + ", at " + LocalTime.now());

        // int slopFactor = 1;
        // try {
        //     slopFactor = Integer.valueOf(
        //         TestLibrary.getExtraProperty("jcov.sleep.multiplier","1"));
        // } catch (NumberFormatException ignore) {}
        // waitTime = waitTime * slopFactor;

        long startTime = System.currentTimeMillis();
        long deadline = TestLibrary.computeDeadline(startTime, waitTime);

        while (true) {
            try {
                Thread.sleep(POLLTIME_MS);
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
                mesg("Starting rmid interrupted, giving up at " +
                    (System.currentTimeMillis() - startTime) + "ms.");
                return;
            }

            try {
                int status = vm.exitValue();
                waitFor(TIMEOUT_SHUTDOWN_MS);
                TestLibrary.bomb("Rmid process exited with status " + status + " after " +
                    (System.currentTimeMillis() - startTime) + "ms.");
            } catch (InterruptedException | TimeoutException e) {
                mesg(e);
            } catch (IllegalThreadStateException ignore) { }

            // The rmid process is alive; check to see whether
            // it responds to a remote call.

            mesg("looking up activation system, at " + LocalTime.now());
            if (lookupSystem(port) != null) {
                /*
                 * We need to set the java.rmi.activation.port value as the
                 * activation system will use the property to determine the
                 * port #.  The activation system will use this value if set.
                 * If it isn't set, the activation system will set it to an
                 * incorrect value.
                 */
                System.setProperty("java.rmi.activation.port", Integer.toString(port));
                mesg("Started successfully after " +
                    (System.currentTimeMillis() - startTime) + "ms, at " + LocalTime.now());
                return;
            }

            mesg("after fail to looking up activation system, at " + LocalTime.now());
            if (System.currentTimeMillis() > deadline) {
                TestLibrary.bomb("Failed to start rmid, giving up after " +
                    (System.currentTimeMillis() - startTime) + "ms.", null);
            }
        }
    }

    /**
     * Destroys rmid and restarts it. Note that this does NOT clean up
     * the log file, because it stores information about restartable
     * and activatable objects that must be carried over to the new
     * rmid instance.
     */
    public void restart() throws IOException {
        destroy();
        options = makeOptions(port, true, true);
        args = makeArgs();

        start(restartTimeout);
    }

    /**
     * Ask rmid to shutdown gracefully using a remote method call.
     * catch any errors that might occur from rmid not being present
     * at time of shutdown invocation. If the remote call is
     * successful, wait for the process to terminate. Return true
     * if the process terminated, otherwise return false.
     */
    private boolean shutdown() throws InterruptedException {
        mesg("shutdown()");
        long startTime = System.currentTimeMillis();
        ActivationSystem system = lookupSystem(port);
        if (system == null) {
            mesg("lookupSystem() returned null after " +
                (System.currentTimeMillis() - startTime) + "ms.");
            return false;
        }

        try {
            mesg("ActivationSystem.shutdown()");
            system.shutdown();
        } catch (Exception e) {
            mesg("Caught exception from ActivationSystem.shutdown():");
            e.printStackTrace();
        }

        try {
            waitFor(TIMEOUT_SHUTDOWN_MS);
            mesg("Shutdown successful after " +
                (System.currentTimeMillis() - startTime) + "ms.");
            return true;
        } catch (TimeoutException ex) {
            mesg("Shutdown timed out after " +
                (System.currentTimeMillis() - startTime) + "ms:");
            ex.printStackTrace();
            return false;
        }
    }

    /**
     * Ask rmid to shutdown gracefully but then destroy the rmid
     * process if it does not exit by itself.  This method only works
     * if rmid is a child process of the current VM.
     */
    public void destroy() {
        if (vm == null) {
            throw new IllegalStateException("can't wait for RMID that isn't running");
        }

        long startTime = System.currentTimeMillis();

        // First, attempt graceful shutdown of the activation system.
        try {
            if (! shutdown()) {
                // Graceful shutdown failed, use Process.destroy().
                mesg("Destroying RMID process.");
                vm.destroy();
                try {
                    waitFor(TIMEOUT_DESTROY_MS);
                    mesg("Destroy successful after " +
                        (System.currentTimeMillis() - startTime) + "ms.");
                } catch (TimeoutException ex) {
                    mesg("Destroy timed out, giving up after " +
                        (System.currentTimeMillis() - startTime) + "ms:");
                    ex.printStackTrace();
                }
            }
        } catch (InterruptedException ie) {
            mesg("Shutdown/destroy interrupted, giving up at " +
                (System.currentTimeMillis() - startTime) + "ms.");
            ie.printStackTrace();
            Thread.currentThread().interrupt();
            return;
        }

        vm = null;
    }

    /**
     * Shuts down rmid and then removes its log file.
     */
    public void cleanup() {
        destroy();
        RMID.removeLog();
    }

    /**
     * Gets the port on which this rmid is listening.
     */
    public int getPort() {
        return port;
    }
}