/*
 * Copyright (c) 2002, 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.jdb;

import nsk.share.*;
import nsk.share.jpda.*;

import java.util.*;
import java.io.*;

import java.util.regex.*;

/**
 * Wrapper of <i>jdb</i>.s
 * This class provides abilities to launch it, to send command,
 * to read reply on the command, to set breakpoint on entry in debugggee's method.
 */
public class Jdb extends LocalProcess implements Finalizable {
    /** File to log <i>stdout</i> stream */
    static final String JDB_STDOUT_FILE = "jdb.stdout";
    /** File to log <i>stderr</i> stream */
    static final String JDB_STDERR_FILE = "jdb.stderr";
    /** File to log input jdb's commands */
    static final String JDB_COMMANDS_FILE = "jdb.commands";

    /** File to log emulated <i>jdb</i> session composed from commands and <i>jdb</i> replies on them  */
    static final String JDB_SESSION_FILE = "jdb.session";

    /** Pattern for message of listening at address. */
    public static final String LISTENING_AT_ADDRESS = "Listening at address:";

    /** Pattern for message of a breakpoint hit. */
    public static final String BREAKPOINT_HIT = "Breakpoint hit:";

    /** Pattern for message of an application exit. */
    public static final String APPLICATION_EXIT = "The application exited";

    /** Pattern for message of an application disconnect. */
    public static final String APPLICATION_DISCONNECTED = "The application has been disconnected";

    /** Pattern for message of connector name in the supported connectors list. */
    public static final String SUPPORTED_CONNECTOR_NAME = "Connector:";

    /** Pattern for message of transport name in the supported connectors list. */
    public static final String SUPPORTED_TRANSPORT_NAME = "Transport: ";

    /** This is jdb's prompt when debuggee is not started nor suspended after breakpoint */
    public static final String SIMPLE_PROMPT = "> ";

    public static final String lineSeparator = System.getProperty("line.separator");

    /** Internal streams handlers */
    private static PrintStream     jdbStdinWriter;
    private static JdbStdoutReader jdbStdoutReader;
    private static JdbStderrReader jdbStderrReader;

    private static PrintStream fout;
    private static PrintStream flog;
    private static PrintStream fin;

    /** Particular ident in a compound prompt, or null if any. */
    private String compoundPromptIdent = null;

    /** <i>Launcher</i> that creates this <i>Jdb</i> object. */
    private static Launcher launcher = null;

    /** Internal buffer to save all not-null string from <i>jdb</i> stdout */
    volatile private static StringBuffer stdoutBuffer = new StringBuffer();

    volatile private Object startNotify = new Object();

    /** Returns <i>Launcher</i> that created this <i>Jdb</i> object. */
    public static Launcher getLauncher() {
        return launcher;
    }

    public void finalizeAtExit() throws Throwable {
        finalize();
    }

    public void finalize() throws Throwable {
        if (fout != null) {
//            fout.flush();
            fout.close();
        }
        if (flog != null) {
            flog.close();
        }
        if (fin != null) {
            fin.close();
        }
        if (jdbStdoutReader != null) {
            jdbStdoutReader.close();
        }
        if (jdbStderrReader != null) {
            jdbStderrReader.close();
        }
        super.finalize();
    }

    /** Create <i>Jdb</i> object. */
    public Jdb (Launcher launcher) {
        super();
        this.launcher = launcher;
    }

    /** Set particular ident for compound prompt; or null for any ident. */
    void setCompoundPromptIdent(String ident) {
        compoundPromptIdent = ident;
    }

    /**
     * Launch <i>jdb</i> with options defined in <i>launchCmdArgs</i>.
     * Full path to <i>jdb</i> must be defined as first element in <i>launchCmdArgs</i>.
     */
    public void launch(String[] launchCmdArgs) throws IOException {
        super.launch(launchCmdArgs);
        redirectStreams();
    }

    /**
     * Launch <i>jdb</i> with options defined in <i>launchCmdLine</i>.
     * Full path to <i>jdb</i> must be defined as first token in <i>launchCmdLine</i>.
     */
    public void launch(String launchCmdLine) throws IOException {
        super.launch(launchCmdLine);
        redirectStreams();
    }

    /**
     * Gets <i>stdin, stdout, stderr</i> streams of the <i>jdb's</i> process and
     * redirects them to special streams handlers.
     */
    private void redirectStreams() {
        OutputStream jdbStdin = this.getStdin();
        if (jdbStdin == null) {
            throw new Failure("jdb stdin after launching is null");
        }
        jdbStdinWriter = new PrintStream(jdbStdin, true);

        String fileStdout = getLauncher().getJdbArgumentHandler().getWorkDir() + File.separator + JDB_STDOUT_FILE;
        InputStream jdbStdout = this.getStdout();
        if (jdbStdout == null) {
            throw new Failure("jdb stdout after launching is null");
        }

        launcher.getLog().display("Creating file for jdb stdout stream: " + fileStdout);
        try {
            fout = new PrintStream(new BufferedOutputStream(new FileOutputStream(fileStdout)));
        } catch (Exception e) {
            e.printStackTrace(getLauncher().getLog().getOutStream());
            throw new Failure("Caught unexpected exception while creating file for jdb stdout stream: " + e);
        }

        String fileCommands = getLauncher().getJdbArgumentHandler().getWorkDir() + File.separator + JDB_COMMANDS_FILE;
        try {
            fin = new PrintStream(new BufferedOutputStream(new FileOutputStream(fileCommands)));
        } catch (Exception e) {
            e.printStackTrace(getLauncher().getLog().getOutStream());
            throw new Failure("Caught unexpected exception while creating file for jdb input commands: " + e);
        }

        String fileSession = getLauncher().getJdbArgumentHandler().getWorkDir() + File.separator + JDB_SESSION_FILE;
        launcher.getLog().display("Creating file for jdb session: " + fileSession);
        try {
            flog = new PrintStream(new BufferedOutputStream(new FileOutputStream(fileSession)));
        } catch (Exception e) {
            e.printStackTrace(getLauncher().getLog().getOutStream());
            throw new Failure("Caught unexpected exception while creating file for jdb session: " + e);
        }

        String fileStderr = getLauncher().getJdbArgumentHandler().getWorkDir() + File.separator + JDB_STDERR_FILE;
        InputStream jdbStderr = this.getStderr();
        if (jdbStderr == null) {
            throw new Failure("jdb stderr after launching is null");
        }

        jdbStdoutReader = new JdbStdoutReader(this);
        startReader(jdbStdoutReader);

        jdbStderrReader = new JdbStderrReader(this, fileStderr);
        startReader(jdbStderrReader);
    }

    /** Starts reading threads for jdb streams. */
    private void startReader (Thread reader) {
        long max = getLauncher().getJdbArgumentHandler().getWaitTime() * 60 * 1000;  // maximum time to wait.
        boolean notified = false;
        synchronized (startNotify) {
            reader.start();
            try {
                startNotify.wait(max);
                notified = true;
            } catch (InterruptedException ie) {
                ie.printStackTrace(getLauncher().getLog().getOutStream());
                throw new Failure("Caught InterruptedException while waiting for start of : " + reader, ie);
            }
        }
        if (!notified) {
            throw new Failure("Main thread was not notified during " + max + " milliseconds" +
                "\n\t waiting for start of : " + reader);
        }
    }

    /**
     * Waits for given stream reader of the <i>jdb's</i> process to finish
     * or interrupts after given timeout.
     */
    private void waitForReader(Thread reader, long timeMillisec) {
        if (reader != null) {
            try {
                reader.join(timeMillisec);
            } catch (InterruptedException ie) {
                ie.printStackTrace(getLauncher().getLog().getOutStream());
                throw new Failure("Caught interrupted exception while waiting for reader finished:\n\t" + ie);
            }
            if (reader.isAlive()) {
                getLauncher().getLog().display("Interrupting reader not finished for timeout: " + timeMillisec + " millisec");
                reader.interrupt();
            }
        }
    }

    /**
     * Waits for all readers of redirected streams of the <i>jdb's</i> process
     * to finish.
     */
    private void waitForAllReaders(long timeMillisec) {
        waitForReader(jdbStdoutReader, timeMillisec);
        waitForReader(jdbStderrReader, timeMillisec);
    }

    /**
      * Wait until the jdb process shutdown or crash
      * and all redirected stream readers finished.
      */
    public int waitFor() throws InterruptedException {
        int exitCode = super.waitFor();
        waitForAllReaders(0);
        return exitCode;
    }

    /**
     * Wait until the process shutdown or crash for given timeout in milliseconds,
     * and all redirected stream readers finished.
     * Returns <code>LocalProcess.PROCESS_IS_ALIVE</code> if process is not terminated
     * after timeout.
     */
    public int waitFor(long timeMillisec) throws InterruptedException {
        int exitCode = super.waitFor(timeMillisec);
        if (exitCode != LocalProcess.PROCESS_IS_ALIVE) {
            waitForAllReaders(timeMillisec);
        }
        return exitCode;
    }

    /**
     * Writes <i>jdbCommand</i> to <i>jdb's</i> input stream.
     */
    public synchronized void sendCommand(String jdbCommand) {

        if (terminated()) {
            throw new Failure("Attempt to send command :" + jdbCommand + "\t to terminated jdb.");
        }

        if (jdbCommand != null) {
            String logCmd;
            if (!jdbCommand.endsWith(lineSeparator)) {
                logCmd = jdbCommand;
                jdbCommand += lineSeparator;
            } else {
                // we don't want to log the line separator
                logCmd = jdbCommand.substring(0, jdbCommand.length() - 1);
            }
            launcher.getLog().display("Sending command: " + logCmd);

            jdbStdinWriter.print(jdbCommand);
            jdbStdinWriter.flush();

            synchronized(flog) {
                flog.print(/*LOG_COMMAND_PREFIX +*/ jdbCommand);
                flog.flush();
            }

            fin.print(jdbCommand);
            fin.flush();

            if (jdbStdinWriter.checkError()) {
                throw new Failure("Unexpected IO error while writing command <" + jdbCommand + "> to jdb stdin stream");
            }
        }
    }

    /**
     * Sends command to <i>jdb's</i> input stream, waits for compound promt received,
     * and then returns reply from <i>jdb's</i> output stream.
     *
     * @param command string representing full command with all arguments if any.
     */
    public String[] receiveReplyFor(String command) {
        return receiveReplyFor(command, true);
    }

    /**
     * Sends command to <i>jdb's</i> input stream, waits for promt received,
     * and then returns reply from <i>jdb's</i> output stream.
     *
     * @param command string representing full command with all arguments if any.
     * @param compoundPromptOnly read <i>output</i> until compound prompt is found.
     */
    public String[] receiveReplyFor(String command, boolean compoundPromptOnly) {
        return receiveReplyFor(command, compoundPromptOnly, 1);
    }

    /**
     * Sends command to <i>jdb's</i> input stream, waits for given number of promts received,
     * and then returns reply from <i>jdb's</i> output stream.
     *
     * @param command string representing full command with all arguments if any.
     * @param compoundPromptOnly read <i>output</i> until compound prompt is found.
     * @param count number of prompt instances to found.
     */
    public String[] receiveReplyFor(String command, boolean compoundPromptOnly, int count) {
        if (command == null) {
           return null;
        }

        int startPos = stdoutBuffer.length();
        sendCommand(command);
        return receiveReply(startPos, compoundPromptOnly, count);
    }

    /**
     * Sends command to <i>jdb's</i> input stream, waits for specified message to be received,
     * and then returns reply from <i>jdb's</i> output stream.
     *
     * @param command string representing full command with all arguments if any.
     * @param waitMsg string representing the message that must be sent back before returing.
     */
    public String[] receiveReplyForWithMessageWait(String command, String waitMsg) {
        if (command == null) {
           return null;
        }

        int startPos = stdoutBuffer.length();
        sendCommand(command);
        waitForMessage(startPos, waitMsg);
        return receiveReply(startPos, true, 1);
    }

    /**
     * Waits for compound prompt and returns reply from <i>jdb</i> stdout
     * beginning from <i>startPos</i> in the <i>stdoutBuffer</i>.
     *
     * @param startPos start position for search in <i>stdoutBuffer</i>.
     */
    public String[] receiveReply(int startPos) {
        return receiveReply(startPos, true);
    }

    /**
     * Waits for particular prompt and returns reply from <i>jdb</i> stdout
     * beginning from <i>startPos</i> in the <i>stdoutBuffer</i>.
     *
     * @param startPos start position for search in <i>stdoutBuffer</i>.
     * @param compoundPromptOnly waits for compound prompt only.
     */
    public String[] receiveReply(int startPos, boolean compoundPromptOnly) {
        return receiveReply(startPos, compoundPromptOnly, 1);
    }

    /**
     * Waits for <i>count</i> number of prompts and returns reply from <i>jdb</i> stdout
     * beginning from <i>startPos</i> in the <i>stdoutBuffer</i>.
     *
     * @param startPos start position for search in <i>stdoutBuffer</i>.
     * @param compoundPromptOnly waits for compound prompt only.
     * @param count number of prompt instances to wait for.
     */
    public String[] receiveReply(int startPos, boolean compoundPromptOnly, int count) {

        int found = waitForPrompt(startPos, compoundPromptOnly, count);

        String reply = stdoutBuffer.substring(startPos, stdoutBuffer.length());
        String[] replyArr = toStringArray(reply);

        // Send reply to the logfile. This complements sendCommand(), which does the same.
        for (int i = 0; i < replyArr.length; i++) {
            launcher.getLog().display("reply[" + i + "]: " + replyArr[i]);
        }

        return replyArr;
    }

    /**
     * Reads <i>JDB_STDOUT_FILE</i> file until prompt is found in the <i>stdoutBuffer</i>.
     *
     * @param startPos start position for search in <i>stdoutBuffer</i>.
     * @param compoundPromptOnly search for compound prompt only.
     * @throws Failure if prompt is not encountered during <i>WaitTime</i>.
     * @return number of prompt instances really found.
     */
    public int waitForPrompt(int startPos, boolean compoundPromptOnly) {
        return waitForPrompt(startPos, compoundPromptOnly, 1);
    }

    /**
     * Reads <i>JDB_STDOUT_FILE</i> file until prompt is found in the <i>stdoutBuffer</i>
     * <i>count</i> times.
     *
     * @param startPos start position for search in <i>stdoutBuffer</i>.
     * @param compoundPromptOnly search for compound prompt only.
     * @throws Failure if prompt is not encountered <i>count</i> times during <i>WaitTime</i>.
     * @return number of prompt instances actually found
     *
     * @see #setCompoundPromptIdent(String)
     */
    public int waitForPrompt(int startPos, boolean compoundPromptOnly, int count) {

        long delta = 200; // time in milliseconds to wait at every iteration.
        long total = 0;    // total time has waited.
        long max = getLauncher().getJdbArgumentHandler().getWaitTime() * 60 * 1000;  // maximum time to wait.

        if (count <= 0) {
            throw new TestBug("Wrong number of promts count in Jdb.waitForPrompt(): " + count);
        }

        Object dummy = new Object();
        while ((total += delta) <= max) {
            int found = 0;

            // check if compound prompt is found
            {
                found = findPrompt(stdoutBuffer, true, startPos);
                if (found >= count) {
                    return found;
                }
            }

            // check also if simple prompt is found
            if (!compoundPromptOnly) {
                found += findPrompt(stdoutBuffer, false, startPos);
                if (found >= count) {
                    return found;
                }
            }

            // exit loop when a debugged application exited
            if (stdoutBuffer.indexOf(APPLICATION_EXIT) >= 0 || stdoutBuffer.indexOf(APPLICATION_DISCONNECTED) >= 0) {
                return found;
            } else if (startPos > 0 && !jdbStdoutReader.isAlive()) {
                return found;
            }

            // sleep for awhile
            synchronized(dummy) {
                try {
                    dummy.wait(delta);
                } catch (InterruptedException ie) {
                    ie.printStackTrace(getLauncher().getLog().getOutStream());
                    throw new Failure("Caught interrupted exception while waiting for jdb prompt:\n\t" + ie);
                }
            }
        }

        Pattern debuggeeExceptionPattern = Pattern.compile("Exception occurred: (?<DebuggeeException>\\S+) \\(uncaught\\)");
        String buf = stdoutBuffer.toString();
        Matcher m = debuggeeExceptionPattern.matcher(buf);

        if (m.find(startPos)) {
            throw new DebuggeeUncaughtException(m.group("DebuggeeException"));
        }

        String times = (count > 1 ? count + " times " : "");
        throw new Failure("Prompt is not received " + times + "during " + total + " milliseconds.");
    }

    /**
     * Reads <i>JDB_STDOUT_FILE</i> file until expected message is found in the <i>stdoutBuffer</i>.
     *
     * @param startPos start position for search in <i>stdoutBuffer</i>.
     * @throws Failure if expected message is not encountered during <i>WaitTime</i>.
     * @return number of messages actually found
     */
    public int waitForMessage(int startPos, String message) {

        long delta = 200; // time in milliseconds to wait at every iteration.
        long total = 0;    // total time has waited.
        long max = getLauncher().getJdbArgumentHandler().getWaitTime() * 60 * 1000;  // maximum time to wait.

        Object dummy = new Object();
        while ((total += delta) <= max) {
            int found = 0;

            // search for message
            {
                found = findMessage(startPos, message);
                if (found > 0) {
                    return found;
                }
            }

            // exit loop when a debugged application exited.
            if (stdoutBuffer.indexOf(APPLICATION_EXIT) >= 0 || stdoutBuffer.indexOf(APPLICATION_DISCONNECTED) >= 0) {
                return found;
            } else if (startPos > 0 && !jdbStdoutReader.isAlive()) {
                return found;
            }

            // spleep for awhile
            synchronized(dummy) {
                try {
                    dummy.wait(delta);
                } catch (InterruptedException ie) {
                    ie.printStackTrace(getLauncher().getLog().getOutStream());
                    throw new Failure("Caught interrupted exception while waiting for jdb reply:\n\t" + ie);
                }
            }

        }

        // If we never recieved the expected reply, display a warning, and also
        // display what we did recieve. This is accomplished by calling receiveReply().
        Log log = getLauncher().getLog();
        log.display("WARNING: message not recieved: " + message);
        log.display("Remaining debugger output follows:");
        receiveReply(startPos);
        throw new Failure("Expected message not received during " + total + " milliseconds:"
                            + "\n\t" + message);
    }

    /**
     * Find message in <i>JDB_STDOUT_FILE</i> file starting from <i>startPos</i>.
     *
     * @param startPos start position for search in <i>stdoutBuffer</i>.
     * @return number of messages actually found
     */
    public int findMessage(int startPos, String message) {
        int bufLength = stdoutBuffer.length();
        int msgLength = message.length();
        int found = 0;

        for (int pos = startPos; pos < bufLength; ) {
            pos = stdoutBuffer.indexOf(message, pos);
            if (pos < 0) break;
            found++;
            pos += msgLength;
        }
        return found;
    }

    /**
     * Searches input lines for <i>jdb</i> prompt of particular kind.
     * starting from <code>startPos</code>.
     * The possible prompt kinds are simple prompt "> " and compound prompt,
     * that looks like '.*\[[0-9]*\] ' regexp on a single line.
     * For example, 'main[1] ' (see setCompoundPromptIdent(String)).
     * <p>
     * In order to make compatible with jdk prior to 1.4.0 avoid using
     * java.util.regex classes.
     *
     * @return number of prompt instances found
     *
     * @see #setCompoundPromptIdent(String)
     */
    int findPrompt(StringBuffer lines, boolean compoundPromptOnly, int startPos) {

        final String nameDelimiters = "-_";

        int noPrompt = -1; // prompt is not found;
        int simplePrompt = 1;
        int complexPrompt = 2;

        int length = lines.length();
        int found = 0;

        // search for simple prompt
        if (!compoundPromptOnly) {
            int promptLength = SIMPLE_PROMPT.length();
            for (int pos = startPos; pos < length; ) {
                pos = lines.indexOf(SIMPLE_PROMPT, pos);
                if (pos < 0) break;
                found++;
                pos += promptLength;
            }
            return found;
        }

        // search for compound prompt
        StringBuffer prompt = new StringBuffer(100);
        searching:
        for (int pos = startPos; pos < length; ) {

            // skip each simbol not suitable for prompt begin
            if (!Character.isLetterOrDigit(lines.charAt(pos))) {
                pos++;
                continue searching;
            }

            // check for compound prompt
            prompt.setLength(0);

            // read name (letters or digits or delimiters)
            while (nameDelimiters.indexOf(lines.charAt(pos)) > 0
                        || Character.isLetterOrDigit(lines.charAt(pos))
                        || lines.charAt(pos) == '-'
                        || lines.charAt(pos) == '_') {
                prompt.append(lines.charAt(pos++));
                if (pos >= length) {
                    break searching;
                }
            }

            // read opening '['
            if (lines.charAt(pos) != '[') {
                continue searching;
            }
            prompt.append(lines.charAt(pos++));
            if (pos >= length) {
                break searching;
            }

            // read number (digits)
            if (!Character.isDigit(lines.charAt(pos))){
                continue searching;
            }
            while (Character.isDigit(lines.charAt(pos))) {
                prompt.append(lines.charAt(pos++));
                if (pos >= length) {
                    break searching;
                }
            }

            // read closing ']'
            if (lines.charAt(pos) != ']') {
                continue searching;
            }
            prompt.append(lines.charAt(pos++));
            if (pos >= length) {
                break searching;
            }

            // read last ' '
            if (lines.charAt(pos) != ' ') {
                continue searching;
            }
            prompt.append(lines.charAt(pos++));

            // check if not particular ident found
            if (compoundPromptIdent != null
                    && !prompt.toString().startsWith(compoundPromptIdent + "[")) {
                continue searching;
            }

            // compound prompt found
            found++;
        }

        return found;
    }

    /**
     * Splits string which may include line separators to string array.
     *
     */
    public static String[] toStringArray (String string) {
        Vector<String> v = new Vector<String>();
        int ind;
        for (ind = 0; ind < string.length(); ) {
            int i = string.indexOf(lineSeparator, ind);
            if (i >= 0) {
                v.add(string.substring(ind, i));
                ind = i + 1;
            } else {
                v.add(string.substring(ind));
                break;
            }
        }
        String[] result = new String [v.size()];
        v.toArray(result);
        return result;
    }

    /**
     * Set breakpoint for debuggee on method invocation.
     */
    public void setBreakpointInMethod(String methodName) {
        String nextCommand = JdbCommand.stop_in + methodName;
        String[] reply = receiveReplyFor(nextCommand);

        Paragrep grep = new Paragrep(reply);
        if (grep.find("Unable to set") > 0) {
            throw new Failure("jdb failed to set breakpoint in method: " + methodName);
        }
        if (grep.find("Set breakpoint") <= 0 && grep.find("Deferring breakpoint") <= 0) {
            throw new Failure("jdb did not set breakpoint in method: " + methodName);
        }
    }

    /**
     * Set deferred breakpoint for debuggee on method invocation.
     * This method must be used before <run> command.
     */
    public void setDeferredBreakpointInMethod(String methodName) {
        String nextCommand = JdbCommand.stop_in + methodName;
        String[] reply = receiveReplyFor(nextCommand, false);

        Paragrep grep = new Paragrep(reply);
        if (grep.find("Unable to set") > 0) {
            throw new Failure("jdb failed to set deffered breakpoint in method: " + methodName);
        }
        if (grep.find("Set breakpoint") <= 0 && grep.find("Deferring breakpoint") <= 0) {
            throw new Failure("jdb did not set deffered breakpoint in method: " + methodName);
        }
    }

    /**
     * Returns true if reply contains breakpoint message.
     */
    public boolean isAtBreakpoint(String[] reply) {
        return isAtBreakpoint(reply, "", "");
    }

    /**
     * Returns true if reply contains breakpoint message in certain method.
     */
    public boolean isAtBreakpoint(String[] reply, String method) {
        return isAtBreakpoint(reply, method, "");
    }

    /**
     * Returns true if reply contains breakpoint message in certain method
     * and in certain thread id.
     */
    public boolean isAtBreakpoint(String[] reply, String method, String thread) {
        boolean result = false;
        Vector<String> v = new Vector<String>();
        Paragrep grep = new Paragrep(reply);

        v.add(BREAKPOINT_HIT);
        if (method.length() > 0) {
            v.add(method);
        }
        if (thread.length() > 0) {
            v.add(thread);
        }
        if (grep.find(v) > 0) {
            result = true;
        }
        return result;
    }

    /**
     * Load and start execution of given debuggee's class with arguments.
     */
    public void startDebuggeeClass(String classWithArgs) {
        String[] reply = receiveReplyFor(JdbCommand.run + " " + classWithArgs);

        // give one more chance to reach breakpoint
        if (!isAtBreakpoint(getTotalReply(), "main")) {
            waitForMessage(0, BREAKPOINT_HIT);
        }
    }

    /**
     * Start execution of pre-loaded debuggee's class.
     */
    public void startDebuggeeClass() {
        String[] reply = receiveReplyFor(JdbCommand.run);

        // give one more chance to reach breakpoint
        if (!isAtBreakpoint(getTotalReply(), "main")) {
            waitForMessage(0, BREAKPOINT_HIT);
        }
    }

    /**
     * Returns as string array all id's for a given <i>threadName</i>.
     */
    public String[] getThreadIds(String threadName) {

        if (!threadName.startsWith("(")) {
            threadName = "(" + threadName;
        }
        if (!threadName.endsWith(")")) {
            threadName = threadName + ")";
        }

        Vector<String> v = new Vector<String>();
        String[] reply = receiveReplyFor(JdbCommand.threads);
        Paragrep grep = new Paragrep(reply);

        String[] found = grep.findStrings(threadName);
        for (int i = 0; i < found.length; i++) {
            String string = found[i];
            int j = string.indexOf(threadName);
            if (j >= 0) {
               j += threadName.length();
               String threadId = string.substring(j, string.indexOf(" ", j));
               v.add(threadId);
            }
        }

        String[] result = new String [v.size()];
        v.toArray(result);
        return result;
    }

    /**
     * Quit <i>jdb</i> using "quit" command.
     */
    public void quit() {
        if (!terminated()) {
            sendCommand(JdbCommand.quit);
        }
    }

    /**
     * Sends "cont" command up to maxTimes until debuggee exit.
     */
    public void contToExit (int maxTimes) {
        boolean exited = false;
        for (int i = 0; i < maxTimes; i++) {
            if (!terminated()) {
                String [] reply = receiveReplyFor(JdbCommand.cont);
                Paragrep grep = new Paragrep(reply);
                if (grep.find(APPLICATION_EXIT) > 0) {
                    exited = true;
                    break;
                }
            } else {
                exited = true;
                break;
            }
        }
        if (!exited) {
            if (terminated()) {
                exited = true;
            } else {
                quit();
                throw new Failure("Debuggee did not exit after " + maxTimes + " <cont> commands");
            }
        }
    }

    /**
     * Returns string array containing all strings from <i>jdb</i> stdout.
     */
    public String[] getTotalReply() {
        return toStringArray(stdoutBuffer.toString());
    }

    /**
     * Prints given message to log files and adds to <i>stdoutBuffer</i>.
     */
    public void logToFile(String s) {
        synchronized(fout) {
            fout.print(s);
            fout.flush();
        }
        synchronized(stdoutBuffer) {
            stdoutBuffer.append(s);
        }
        synchronized(flog) {
            flog.print(s);
            flog.flush();
        }
    }


    /**
     *  Starts jdb with attaching connector. Makes several tries during <i>waitTime</i>
     *  until success. Unsuccessful launches are caused that the debuggee is not yet
     *  ready to accept debugger.
     */
    public static Jdb startAttachingJdb (Launcher launcher, String[] jdbCmdArgs, String message)
            throws IOException  {
        Jdb jdb = null;

        long delta = Launcher.DEBUGGEE_START_DELAY; // time in milliseconds to wait at every iteration.
        long max = getLauncher().getJdbArgumentHandler().getWaitTime() * 60 * 1000;  // maximum time to wait.

        int result = -1;
        boolean found = false;

        long start = System.currentTimeMillis();

        while (!found && (System.currentTimeMillis() - start)<= max) {

            jdb = new Jdb(launcher);
            jdb.launch(jdbCmdArgs);

            while (!found && (System.currentTimeMillis() - start)<= max) {

                try {
                    Thread.currentThread().sleep(delta);
                } catch (InterruptedException ie) {
                    ie.printStackTrace(getLauncher().getLog().getOutStream());
                    throw new Failure("Caught unexpected InterruptedException while sleep in waiting for debuggee's start:\n\t"
                       + ie);
                }

                if (jdb.terminated() ||
                    !jdbStdoutReader.isAlive() ||
                    stdoutBuffer.indexOf(APPLICATION_EXIT) >= 0 ||
                    stdoutBuffer.indexOf(APPLICATION_DISCONNECTED) >= 0) {

                    System.out.println("Unsuccessful launch of attaching jdb. Next try...");
                    try {
                        jdb.finalize();
                    } catch (Throwable t) {
                        t.printStackTrace(getLauncher().getLog().getOutStream());
                        throw new Failure("Caught unexpected error while finalizing jdb: " + t);
                    }
                    break;

                } else if (stdoutBuffer.length() > 0) {
                    result = stdoutBuffer.indexOf(message);
                    if (result >= 0) {
                        found = true; // exit loop
                    }
                }
            }

        }

        if (result < 0) {
            throw new Failure("Launched jdb could not attach to debuggee during " + max + " milliseconds.");
        }

        return jdb;
    }

    /**
     *  Waits for jdb to print message about listening at address for connection,
     *  and returns this address string.
     */
    public String waitForListeningJdb() {

        waitForMessage(0, LISTENING_AT_ADDRESS);
        int msgStart = stdoutBuffer.indexOf(LISTENING_AT_ADDRESS);
        int msgEnd = stdoutBuffer.indexOf("\n", msgStart);
        int promptLen = LISTENING_AT_ADDRESS.length();

        /*
         * The LISTENING_AT_ADDRESS string and the terminating "\n"
         * may or may not be included in the same read so we allow
         * this message to be terminated by "\n" or NULL.
         */
        if (msgEnd < 0) {
            msgEnd = stdoutBuffer.length();
        }

        if (msgEnd <= 0 || msgEnd - msgStart <= promptLen) {
            throw new Failure("Unknown format of message: " + LISTENING_AT_ADDRESS);
        }

        int addrStart = msgStart + promptLen;
        String address = stdoutBuffer.substring(addrStart, msgEnd).trim();

        if (address.length() <= 0) {
            throw new Failure("Empty address in message: " + LISTENING_AT_ADDRESS);
        }

        return address;
    }

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

    class JdbStdoutReader extends Thread {
        private Jdb jdb = null;
        private InputStream in = null;

        volatile boolean stop = false;

        public JdbStdoutReader (Jdb jdb) {
            super("jdb stdout reader");
            this.jdb = jdb;
            this.in = jdb.getStdout();
            if (in == null) {
                throw new Failure("Can not get jdb stdout stream");
            }
            this.setDaemon(true);
        }

        public String toString() {
            return getClass().getName() + '@' + Integer.toHexString(hashCode());
        }

        public void run() {
            synchronized(jdb.startNotify) {
                jdb.startNotify.notifyAll();
            }

            long delta = 10; // time in milliseconds to wait at every iteration.
            boolean jdbWasTerminated = false;
            while (!stop) {
                if(jdb.terminated())
                        jdbWasTerminated = true;
                try {
                    int size = in.available();
                    if (size > 0) {
                        byte[] buffer = new byte [size];
                        int result = in.read(buffer, 0, size);
                        if (result < 0) {
                            throw new Failure("No bytes read from jdb's output stream ");
                        } else if (result < size) {
                            throw new Failure("Number bytes read from jdb's output stream are less than available " +
                                 "\n\t available : " + size + ", read : " + result);
                        }
                        logToFile(new String(buffer, 0, result));
                    }
                } catch (Exception e) {
                    e.printStackTrace(jdb.getLauncher().getLog().getOutStream());
                    throw new Failure("Caught unexpected exception while reading jdb's stdout stream: " + e);
                }
                if(jdbWasTerminated)
                        break;
                try {
                    sleep(delta);
                } catch (InterruptedException ie) {
                    ie.printStackTrace(jdb.getLauncher().getLog().getOutStream());
                    throw new Failure("Caught interrupted exception while waiting for jdb reply:\n\t" + ie);
                }
            }
        }

        public void close() {
            stop = true;
            try {
                if (in != null) {
                    in.close();
                }
            } catch (IOException ioe) {
                ioe.printStackTrace(jdb.getLauncher().getLog().getOutStream());
                throw new Failure("Caught unexpected IOException while closing jdb stdout stream: " + ioe);
            }
        }
    }

    /** Handler for <i>jdb</i> stderr stream. */
    class JdbStderrReader extends Thread {

        private Jdb jdb = null;
        private volatile boolean cancelled = false;
        private boolean empty = true;

        private BufferedReader bin;
        private PrintStream fout;
        private String fileName;

        JdbStderrReader (Jdb jdb, String jdbStderrFile) {
            super("jdb stderr reader");
            this.jdb = jdb;
            InputStream in = jdb.getStderr();
            if (in == null) {
                throw new Failure("Can not get jdb stderr stream");
            }
            this.bin = new BufferedReader(new InputStreamReader(in));
            this.setDaemon(true);

            this.fileName = jdbStderrFile;

            launcher.getLog().display("Creating file for jdb stderr stream: " + fileName);
            try {
                this.fout = new PrintStream(new BufferedOutputStream(new FileOutputStream(fileName)));
            } catch (Exception e) {
                e.printStackTrace(jdb.getLauncher().getLog().getOutStream());
                throw new Failure("Caught unexpected exception while creating file for jdb stderr stream: " + e);
            }
        }

        public void run () {
            synchronized(jdb.startNotify) {
                jdb.startNotify.notifyAll();
            }

            long delta = 10; // time in milliseconds to wait at every iteration.

            while (!cancelled) {
                String line = null;
                try {
                    line = bin.readLine();
                    if (line == null)
                        break; //EOF
                } catch (IOException ioe) {
                    ioe.printStackTrace(jdb.getLauncher().getLog().getOutStream());
                    throw new Failure("Caught unexpected IOException while reading from jdb stderr: " + ioe);
                }

                if (line != null) {
                   empty = false;
                   logToFile(line);
                }

                try {
                    sleep(delta);
                } catch (InterruptedException ie) {
                    throw new Failure("Caught interrupted exception while waiting for jdb reply:\n\t" + ie);
                }
            }
            close();
        }

        /**
         * Signal to <i>run()</i> method that it should terminate,
         * and wait until it is finished.
         */
        public void cancel () {
            cancelled = true;
            while (this.isAlive()) {
                try {
                    this.join();
                } catch (InterruptedException ie) {
                    close();
                    throw new Failure("Caught InterruptedException while waiting for JdbStderrReader termination " + ie);
                }
            }
            close();
        }

        public void close() {
            if (fout != null) {
                synchronized (fout) {
                    fout.close();
                }
            }

            try {
                if (bin != null) {
                    bin.close();
                }
            } catch (IOException ioe) {
                ioe.printStackTrace(jdb.getLauncher().getLog().getOutStream());
                throw new Failure("Caught unexpected IOException while closing jdb stderr stream: " + ioe);
            }
            if (!empty) {
                 // Should not throw exception here because of non-empty stderr in case of unsuccessful launch of attaching jdb.
                jdb.getLauncher().getLog().display("JdbStderrReader: jdb's stderr is not empty. Check jdb.stderr file");
            }
        }

        public String getFileName () {
            return this.fileName;
        }

        public void logToFile(String line) {
            synchronized (fout) {
                fout.println(line);
                fout.flush();
            }
        }
    } // end of JdbStderrReader
}  // end of Jdb
