1 /*
   2  * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 package lib.jdb;
  25 
  26 import java.io.ByteArrayOutputStream;
  27 import java.io.IOException;
  28 import java.io.OutputStream;
  29 import java.io.PrintWriter;
  30 import java.util.ArrayList;
  31 import java.util.Arrays;
  32 import java.util.Collections;
  33 import java.util.LinkedList;
  34 import java.util.List;
  35 import java.util.concurrent.TimeUnit;
  36 import java.util.regex.Pattern;
  37 import java.util.stream.Collectors;
  38 import jdk.test.lib.JDKToolFinder;
  39 import jdk.test.lib.Utils;
  40 import jdk.test.lib.process.StreamPumper;
  41 
  42 public class Jdb implements AutoCloseable {
  43     public Jdb(String... args) {
  44         ProcessBuilder pb = new ProcessBuilder(JDKToolFinder.getTestJDKTool("jdb"));
  45         pb.command().addAll(Arrays.asList(args));
  46         try {
  47             jdb = pb.start();
  48         } catch (IOException ex) {
  49             throw new RuntimeException("failed to launch pdb", ex);
  50         }
  51         try {
  52             StreamPumper stdout = new StreamPumper(jdb.getInputStream());
  53             StreamPumper stderr = new StreamPumper(jdb.getErrorStream());
  54 
  55             stdout.addPump(new StreamPumper.StreamPump(outputHandler));
  56             stderr.addPump(new StreamPumper.StreamPump(outputHandler));
  57 
  58             stdout.process();
  59             stderr.process();
  60 
  61             inputWriter = new PrintWriter(jdb.getOutputStream(), true);
  62         } catch (Throwable ex) {
  63             // terminate jdb if something went wrong
  64             jdb.destroy();
  65             throw ex;
  66         }
  67     }
  68 
  69     private final Process jdb;
  70     private final OutputHandler outputHandler = new OutputHandler();
  71     private final PrintWriter inputWriter;
  72     private final List<String> jdbOutput = new LinkedList<>();
  73 
  74     private static final String lineSeparator = System.getProperty("line.separator");
  75     // wait time before check jdb output (in ms)
  76     private static final long sleepTime = 1000;
  77     // max time to wait for  jdb output (in ms)
  78     private static final long timeout = Utils.adjustTimeout(60000);
  79 
  80     // pattern for message of a breakpoint hit
  81     public static final String BREAKPOINT_HIT = "Breakpoint hit:";
  82     // pattern for message of an application exit
  83     public static final String APPLICATION_EXIT = "The application exited";
  84     // pattern for message of an application disconnect
  85     public static final String APPLICATION_DISCONNECTED = "The application has been disconnected";
  86 
  87 
  88     @Override
  89     public void close() throws Exception {
  90         shutdown();
  91     }
  92 
  93     // waits until the process shutdown or crash
  94     public boolean waitFor(long timeout, TimeUnit unit) {
  95         try {
  96             return jdb.waitFor(Utils.adjustTimeout(timeout), unit);
  97         } catch (InterruptedException e) {
  98             return false;
  99         }
 100     }
 101 
 102     public void shutdown() {
 103         // shutdown jdb
 104         if (jdb.isAlive()) {
 105             try {
 106                 quit();
 107                 // wait some time after the command for the process termination
 108                 waitFor(10, TimeUnit.SECONDS);
 109             } finally {
 110                 if (jdb.isAlive()) {
 111                     jdb.destroy();
 112                 }
 113             }
 114         }
 115     }
 116 
 117 
 118     // waits until string {@pattern} appears in the jdb output, within the last {@code lines} lines.
 119     /* Comment from original /test/jdk/com/sun/jdi/ShellScaffold.sh
 120         # Now we have to wait for the next jdb prompt.  We wait for a pattern
 121         # to appear in the last line of jdb output.  Normally, the prompt is
 122         #
 123         # 1) ^main[89] @
 124         #
 125         # where ^ means start of line, and @ means end of file with no end of line
 126         # and 89 is the current command counter. But we have complications e.g.,
 127         # the following jdb output can appear:
 128         #
 129         # 2) a[89] = 10
 130         #
 131         # The above form is an array assignment and not a prompt.
 132         #
 133         # 3) ^main[89] main[89] ...
 134         #
 135         # This occurs if the next cmd is one that causes no jdb output, e.g.,
 136         # 'trace methods'.
 137         #
 138         # 4) ^main[89] [main[89]] .... > @
 139         #
 140         # jdb prints a > as a prompt after something like a cont.
 141         # Thus, even though the above is the last 'line' in the file, it
 142         # isn't the next prompt we are waiting for after the cont completes.
 143         # HOWEVER, sometimes we see this for a cont command:
 144         #
 145         #   ^main[89] $
 146         #      <lines output for hitting a bkpt>
 147         #
 148         # 5) ^main[89] > @
 149         #
 150         # i.e., the > prompt comes out AFTER the prompt we we need to wait for.
 151     */
 152     // compile regexp once
 153     private final static String promptPattern = "[a-zA-Z0-9_-][a-zA-Z0-9_-]*\\[[1-9][0-9]*\\] [ >]*$";
 154     final static Pattern PROMPT_REGEXP = Pattern.compile(promptPattern);
 155 
 156     public List<String> waitForPrompt(int lines, boolean allowExit) {
 157         return waitForPrompt(lines, allowExit, PROMPT_REGEXP);
 158     }
 159 
 160     // jdb prompt when debuggee is not started and is not suspended after breakpoint
 161     private static final String SIMPLE_PROMPT = "> ";
 162     public List<String> waitForSimplePrompt(int lines, boolean allowExit) {
 163         return waitForPrompt(lines, allowExit, Pattern.compile(SIMPLE_PROMPT));
 164     }
 165 
 166     private List<String> waitForPrompt(int lines, boolean allowExit, Pattern promptRegexp) {
 167         long startTime = System.currentTimeMillis();
 168         while (System.currentTimeMillis() - startTime < timeout) {
 169             try {
 170                 Thread.sleep(sleepTime);
 171             } catch (InterruptedException e) {
 172                 // ignore
 173             }
 174             synchronized (outputHandler) {
 175                 if (!outputHandler.updated()) {
 176                     try {
 177                         outputHandler.wait(sleepTime);
 178                     } catch (InterruptedException e) {
 179                         // ignore
 180                     }
 181                 } else {
 182                     // if something appeared in the jdb output, reset the timeout
 183                     startTime = System.currentTimeMillis();
 184                 }
 185             }
 186             List<String> reply = outputHandler.get();
 187             if ((promptRegexp.flags() & Pattern.MULTILINE) > 0) {
 188                 String replyString = reply.stream().collect(Collectors.joining(lineSeparator));
 189                 if (promptRegexp.matcher(replyString).find()) {
 190                     logJdb(reply);
 191                     return outputHandler.reset();
 192                 }
 193             } else {
 194                 for (String line : reply.subList(Math.max(0, reply.size() - lines), reply.size())) {
 195                     if (promptRegexp.matcher(line).find()) {
 196                         logJdb(reply);
 197                         return outputHandler.reset();
 198                     }
 199                 }
 200             }
 201             if (!jdb.isAlive()) {
 202                 // ensure we get the whole output
 203                 reply = outputHandler.reset();
 204                 logJdb(reply);
 205                 if (!allowExit) {
 206                     throw new RuntimeException("waitForPrompt timed out after " + (timeout/1000)
 207                             + " seconds, looking for '" + promptRegexp.pattern() + "', in " + lines + " lines");
 208                 }
 209                 return reply;
 210             }
 211         }
 212         // timeout
 213         logJdb(outputHandler.get());
 214         throw new RuntimeException("waitForPrompt timed out after " + (timeout/1000)
 215                 + " seconds, looking for '" + promptRegexp.pattern() + "', in " + lines + " lines");
 216     }
 217 
 218     public List<String> command(JdbCommand cmd) {
 219         if (!jdb.isAlive()) {
 220             if (cmd.allowExit) {
 221                 // return remaining output
 222                 return outputHandler.reset();
 223             }
 224             throw new RuntimeException("Attempt to send command '" + cmd.cmd + "' to terminated jdb");
 225         }
 226 
 227         log("> " + cmd.cmd);
 228 
 229         inputWriter.println(cmd.cmd);
 230 
 231         if (inputWriter.checkError()) {
 232             throw new RuntimeException("Unexpected IO error while writing command '" + cmd.cmd + "' to jdb stdin stream");
 233         }
 234 
 235         return waitForPrompt(1, cmd.allowExit, cmd.waitForPattern);
 236     }
 237 
 238     public List<String> command(String cmd) {
 239         return command(new JdbCommand(cmd));
 240     }
 241 
 242     // sends "cont" command up to maxTimes until debuggee exit
 243     public void contToExit(int maxTimes) {
 244         boolean exited = false;
 245         JdbCommand cont = JdbCommand.cont().allowExit();
 246         for (int i = 0; i < maxTimes && jdb.isAlive(); i++) {
 247             String reply = command(cont).stream().collect(Collectors.joining(lineSeparator));
 248             if (reply.contains(APPLICATION_EXIT)) {
 249                 exited = true;
 250                 break;
 251             }
 252         }
 253         if (!exited && jdb.isAlive()) {
 254             throw new RuntimeException("Debuggee did not exit after " + maxTimes + " <cont> commands");
 255         }
 256     }
 257 
 258     // quits jdb by using "quit" command
 259     public void quit() {
 260         command(JdbCommand.quit());
 261     }
 262 
 263     void log(String s) {
 264         System.out.println(s);
 265     }
 266 
 267     private void logJdb(List<String> reply) {
 268         jdbOutput.addAll(reply);
 269         reply.forEach(s -> log("[jdb] " + s));
 270     }
 271 
 272     // returns the whole jdb output as a string
 273     public String getJdbOutput() {
 274         return jdbOutput.stream().collect(Collectors.joining(lineSeparator));
 275     }
 276 
 277     // handler for out/err of the pdb process
 278     private class OutputHandler extends OutputStream {
 279         // there are 2 buffers:
 280         // outStream - data from the process stdout/stderr after last get() call
 281         // cachedData - data collected at get(), cleared by reset()
 282 
 283         private final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
 284         // if the last line in the reply had EOL, the list's last element is empty
 285         private final List<String> cachedData = new ArrayList<>();
 286 
 287         @Override
 288         public synchronized void write(int b) throws IOException {
 289             outStream.write((byte)(b & 0xFF));
 290             notifyAll();
 291         }
 292         @Override
 293         public synchronized void write(byte b[], int off, int len) throws IOException {
 294             outStream.write(b, off, len);
 295             notifyAll();
 296         }
 297 
 298         // gets output after the last {@ reset}.
 299         // returned data becomes invalid after {@reset}.
 300         public synchronized List<String> get() {
 301             if (updated()) {
 302                 // we don't want to discard empty lines
 303                 String[] newLines = outStream.toString().split("\\R", -1);
 304                 if (!cachedData.isEmpty()) {
 305                     // concat the last line if previous data had no EOL
 306                     newLines[0] = cachedData.remove(cachedData.size()-1) + newLines[0];
 307                 }
 308                 cachedData.addAll(Arrays.asList(newLines));
 309                 outStream.reset();
 310             }
 311             return Collections.unmodifiableList(cachedData);
 312         }
 313 
 314         // clears last replay (does not touch replyStream)
 315         // returns list as the last get()
 316         public synchronized List<String> reset() {
 317             List<String> result = new ArrayList<>(cachedData);
 318             cachedData.clear();
 319             return result;
 320         }
 321 
 322         // tests if there are some new data after the last lastReply() call
 323         public synchronized boolean updated() {
 324             return outStream.size() > 0;
 325         }
 326     }
 327 }
 328