/*
 * Copyright (c) 2001, 2018, 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.
 */

package nsk.share.jpda;

import nsk.share.*;
import java.io.*;
import java.net.*;

/**
 * This class is used to control debugee VM process.
 * <p>
 * Object of this class is constructed by <code>DebugeeBinder</code>
 * and is used as a mirror of debugee VM process.
 * It provides abilities to launch such process,
 * redirect standard output streams, wait for process terminates
 * or kill the process, and so on.
 * <p>
 * This is an abstract class that declares abstract methods to control
 * debugee VM process.
 * Derived classes should implement these methods corresponding to the mode
 * that the process should be started in (locally, remotely or manually).
 * <p>
 * Particular derived classes <code>nsk.share.jdi.Debugee</code> and
 * <code>nsk.share.jdwp.Debugee</code> provides additional abilities
 * to control debugee VM using JDI or JDWP specific features.
 *
 * @see DebugeeBinder
 *
 * @see nsk.share.jdi.Debugee
 * @see nsk.share.jdwp.Debugee
 */
abstract public class DebugeeProcess extends FinalizableObject {

    /** Default prefix for log messages. */
    public static final String LOG_PREFIX = "binder> ";
    public static final String DEBUGEE_STDOUT_LOG_PREFIX = "debugee.stdout> ";
    public static final String DEBUGEE_STDERR_LOG_PREFIX = "debugee.stderr> ";

    /** Messages prefix. */
    protected String prefix = LOG_PREFIX;

    /** Binder that creates this debugee process. */
    protected DebugeeBinder binder = null;

    /** Messages log from binder. */
    protected Log log = null;

    /** Communicational channel between debuger and debugee. */
    protected IOPipe pipe = null;

    /** Argument handler from binder. */
    protected DebugeeArgumentHandler argumentHandler = null;

    /** Need or not to check debuggee process termination at exit. */
    protected boolean checkTermination = false;

    /** Debugee VM process or <i>null</i> if not available. */
    protected Process process = null;

    /** Make new <code>Debugee</code> object for the given binder. */
    protected DebugeeProcess (DebugeeBinder binder) {
        this.binder = binder;
        this.log = binder.getLog();
    }

    /**
     * Return already bound ServerSocket for IOPipe connection or null.
     */
    protected ServerSocket getPipeServerSocket() {
        return binder.getPipeServerSocket();
    }

    /** Return <code>DebugeeArgumentHandler</code> of the debugee object. */
    public DebugeeArgumentHandler getArgumentHandler() {
        return binder.getArgumentHandler();
    }

    /** Return <code>Log</code> of the debugee object. */
    public Log getLog() {
        return log;
    }

    /** Return <code>Process</code> object associated with debugee VM or <i>null</i>. */
    public Process getProcess() {
        return process;
    }

    // --------------------------------------------------- //

    /** Created and return new IOPipe channel to the debugee VM. */
    public IOPipe createIOPipe() {
        if (pipe != null) {
            throw new TestBug("IOPipe channel is already created");
        }
        pipe = new IOPipe(this);
        return pipe;
    }

    /** Return IOPipe channel or null if not yet ctreated. */
    public IOPipe getIOPipe() {
        return pipe;
    }

    /** Receive and return signal from the debugee VM via IOPipe channel.
     *
     *  @throws TestBug if IOPipe channel is not created
     */
    public String receiveSignal() {
        if ( pipe == null )
            throw new TestBug("IOPipe channel is not initialized");
        return pipe.readln();
    }

    /** Receive an expected <code>signal</code> from the debugee VM via IOPipe channel.
     *
     *  @throws Failure if received signal is not equal to the expected one
     *  @throws TestBug if IOPipe channel is not created
     */
    public void receiveExpectedSignal(String signal) {
        String line = receiveSignal();
        if (line == null || !line.equals(signal))
            throw new Failure("Received unexpected signal from debugee: " + line);
        display("Received expected signal from debugee: " + signal);
    }

    /** Send <code>signal</code> to the debugee VM via IOPipe channel.
     *
     *  @throws TestBug if IOPipe channel is not defined created.
     */
    public void sendSignal(String signal) {
        if (pipe == null)
            throw new TestBug("IOPipe channel is not initialized");
        pipe.println(signal);
    }


    // --------------------------------------------------- //

    /** Wait until the debugee VM shutdown or crash. */
    abstract protected int waitForDebugee () throws InterruptedException;

    /** Kill the debugee VM. */
    abstract protected void killDebugee ();

    /** Check whether the debugee VM has been terminated. */
    abstract public boolean terminated ();

    /** Return the debugee VM exit status. */
    abstract public int getStatus ();

    /** Get a pipe to write to the debugee's stdin stream. */
    abstract protected OutputStream getInPipe ();

    /** Get a pipe to read the debugee's stdout stream. */
    abstract protected InputStream getOutPipe ();

    /** Get a pipe to read the debugee's stderr stream. */
    abstract protected InputStream getErrPipe ();

    // --------------------------------------------------- //

    /**
     * Wait until the debugee VM shutdown or crash,
     * and let finish its stdout, stderr, and stdin
     * redirectors (if any).
     *
     * @return  Debugee process exit code.
     * @see #waitForRedirectors(long)
     */
    public int waitFor () {
        long timeout = binder.getArgumentHandler().getWaitTime() * 60 * 1000;
        int exitCode = 0;
        try {
            exitCode = waitForDebugee();
        } catch (InterruptedException ie) {
            ie.printStackTrace(log.getOutStream());
            throw new Failure("Caught exception while waiting for debuggee process: \n\t" + ie);
        }
        waitForRedirectors(timeout);
        if (process != null) {
            process.destroy();
        }
        return exitCode;
    }

    /**
     * Wait until the debugee VM redirectors to complete for specified <code>timeout</code>.
     *
     * @see #waitFor()
     */
    public void waitForRedirectors (long timeout) {
        try {
            if (stdinRedirector != null) {
                if (stdinRedirector.isAlive()) {
                    stdinRedirector.join(timeout);
                    if (stdinRedirector.isAlive()) {
                        log.complain("Timeout for waiting STDIN redirector exceeded");
                        stdinRedirector.interrupt();
                    }
                }
                stdinRedirector = null;
            };
            if (stdoutRedirector != null) {
                if (stdoutRedirector.isAlive()) {
                    stdoutRedirector.join(timeout);
                    if (stdoutRedirector.isAlive()) {
                        log.complain("Timeout for waiting STDOUT redirector exceeded");
                        stdoutRedirector.interrupt();
                    }
                }
                stdoutRedirector = null;
            };
            if (stderrRedirector != null) {
                if (stderrRedirector.isAlive()) {
                    stderrRedirector.join(timeout);
                    if (stderrRedirector.isAlive()) {
                        log.complain("Timeout for waiting STDERR redirector exceeded");
                        stderrRedirector.interrupt();
                    }
                }
                stderrRedirector = null;
            };
        } catch (InterruptedException ie) {
            ie.printStackTrace(log.getOutStream());
            throw new Failure("Caught exception while waiting for debuggee output redirectors: \n\t"
                                + ie);
        }
    }

    // --------------------------------------------------- //

    /**
     * Get a pipe to write to the debugee's stdin stream,
     * or throw TestBug exception is redirected.
     */
    final public OutputStream getStdin () {
        if (stdinRedirector != null)
            throw new TestBug("Debugee's stdin is redirected");
        return getInPipe();
    }

    /**
     * Get a pipe to read the debugee's stdout stream,
     * or throw TestBug exception is redirected.
     */
    final public InputStream getStdout () {
        if (stdoutRedirector != null)
            throw new TestBug("Debugee's stdout is redirected");
        return getOutPipe();
    }

    /**
     * Get a pipe to read the debugee's stderr stream,
     * or throw TestBug exception is redirected.
     */
    final public InputStream getStderr () {
        if (stderrRedirector != null)
            throw new TestBug("Debugee's stderr is redirected");
        return getErrPipe();
    }

    // --------------------------------------------------- //

    private IORedirector stdoutRedirector = null;
    private IORedirector stderrRedirector = null;
    private IORedirector stdinRedirector = null;

//    /**
//     * Start a thread redirecting the given <code>in</code> stream
//     * to the debugee's stdin. If the debugee's stdin was already
//     * redirected, the TestBug exception is thrown.
//     */
//    final public void redirectStdin(InputStream in) {
//        if (stdinRedirector != null)
//            throw new TestBug("the debugee's stdin is already redirected");
//        stdinRedirector = new IORedirector(in,getInPipe());
//        stdinRedirector.setName("IORedirector for stdin");
//        stdinRedirector.setDaemon(true);
//        stdinRedirector.start();
//    }

    /**
     * Start thread redirecting the debugee's stdout to the
     * given <code>out</code> stream. If the debugee's stdout
     * was already redirected, the TestBug exception is thrown.
     *
     * @deprecated Use redirectStdout(Log, String) instead.
     */
    public void redirectStdout(OutputStream out) {
        if (stdoutRedirector != null) {
            return;
        }
//            throw new TestBug("Debugee's stdout is already redirected");
        stdoutRedirector = new IORedirector(getOutPipe(),out);
        stdoutRedirector.setPrefix(DEBUGEE_STDOUT_LOG_PREFIX);
        stdoutRedirector.setName("IORedirector for stdout");
        stdoutRedirector.setDaemon(true);
        stdoutRedirector.start();
    }

    /**
     * Start thread redirecting the debugee's stdout to the
     * given <code>Log</code>. If the debugee's stdout
     * was already redirected, the TestBug exception is thrown.
     */
    public void redirectStdout(Log log, String prefix) {
        if (stdoutRedirector != null) {
//            stdoutRedirector.setPrefix(prefix);
            return;
//            throw new TestBug("the debugee's stdout is already redirected");
        }
        stdoutRedirector = new IORedirector(new BufferedReader(new InputStreamReader(getOutPipe())), log, prefix);
        stdoutRedirector.setName("IORedirector for stdout");
        stdoutRedirector.setDaemon(true);
        stdoutRedirector.start();
    }

    /**
     * Start thread redirecting the debugee's stderr to the
     * given <code>err</code> stream. If the debugee's stderr
     * was already redirected, the TestBug exception is thrown.
     *
     * @deprecated Use redirectStderr(Log, String) instead.
     */
    public void redirectStderr(OutputStream err) {
        if (stderrRedirector != null) {
            return;
        }
//            throw new TestBug("Debugee's stderr is already redirected");
        stderrRedirector = new IORedirector(getErrPipe(),err);
        stderrRedirector.setPrefix(DEBUGEE_STDERR_LOG_PREFIX);
        stdoutRedirector.setName("IORedirector for stderr");
        stderrRedirector.setDaemon(true);
        stderrRedirector.start();
    }

    /**
     * Start thread redirecting the debugee's stderr to the
     * given <code>Log</code>. If the debugee's stderr
     * was already redirected, the TestBug exception is thrown.
     */
    public void redirectStderr(Log log, String prefix) {
        if (stderrRedirector != null) {
//            stderrRedirector.setPrefix(prefix);
            return;
//            throw new TestBug("Debugee's stderr is already redirected");
        }
        stderrRedirector = new IORedirector(new BufferedReader(new InputStreamReader(getErrPipe())), log, prefix);
        stdoutRedirector.setName("IORedirector for stderr");
        stderrRedirector.setDaemon(true);
        stderrRedirector.start();
    }

    /**
     * Start thread redirecting the debugee's stdout/stderr to the
     * given <code>Log</code> using standard prefixes.
     * If the debugee's stdout/stderr were already redirected,
     * the TestBug exception is thrown.
     */
    public void redirectOutput(Log log) {
        redirectStdout(log, "debugee.stdout> ");
        redirectStderr(log, "debugee.stderr> ");
    }
    // --------------------------------------------------- //

    /**
     * Kill the debugee VM if it is not terminated yet.
     *
     * @throws Throwable if any throwable exception is thrown during finalization
     */
    public void close() {
        if (checkTermination) {
            if (!terminated()) {
                complain("Debugee VM has not exited correctly: trying to kill it");
                killDebugee();
            }
            checkTermination = false;
        }
    }

    // --------------------------------------------------- //

    /**
     * Display log message with prefix.
     */
    protected void display(String message) {
        log.display(prefix + message);
    }

    /**
     * Complain about error with specified message.
     */
    protected void complain(String message) {
        log.complain(prefix + message);
    }

    /**
     * Finalize debuggee VM wrapper by invoking <code>close()</code>.
     *
     * @throws Throwable if any throwable exception is thrown during finalization
     */
    protected void finalize() throws Throwable {
        close();
        super.finalize();
    }
}
