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 String promptPattern = "[a-zA-Z0-9_-][a-zA-Z0-9_-]*\\[[1-9][0-9]*\\] [ >]*$";
 154     private final Pattern promptRegexp = Pattern.compile(promptPattern);
 155     public List<String> waitForPrompt(int lines, boolean allowExit) {
 156         return waitForPrompt(lines, allowExit, promptRegexp);
 157     }
 158 
 159     // jdb prompt when debuggee is not started and is not suspended after breakpoint
 160     private static final String SIMPLE_PROMPT = "> ";
 161     public List<String> waitForSimplePrompt(int lines, boolean allowExit) {
 162         return waitForPrompt(lines, allowExit, Pattern.compile(SIMPLE_PROMPT));
 163     }
 164 
 165     private List<String> waitForPrompt(int lines, boolean allowExit, Pattern promptRegexp) {
 166         long startTime = System.currentTimeMillis();
 167         while (System.currentTimeMillis() - startTime < timeout) {
 168             try {
 169                 Thread.sleep(sleepTime);
 170             } catch (InterruptedException e) {
 171                 // ignore
 172             }
 173             synchronized (outputHandler) {
 174                 if (!outputHandler.updated()) {
 175                     try {
 176                         outputHandler.wait(sleepTime);
 177                     } catch (InterruptedException e) {
 178                         // ignore
 179                     }
 180                 } else {
 181                     // if something appeared in the jdb output, reset the timeout
 182                     startTime = System.currentTimeMillis();
 183                 }
 184             }
 185             List<String> reply = outputHandler.get();
 186             for (String line: reply.subList(Math.max(0, reply.size() - lines), reply.size())) {
 187                 if (promptRegexp.matcher(line).find()) {
 188                     logJdb(reply);
 189                     return outputHandler.reset();
 190                 }
 191             }
 192             if (!jdb.isAlive()) {
 193                 // ensure we get the whole output
 194                 reply = outputHandler.reset();
 195                 logJdb(reply);
 196                 if (!allowExit) {
 197                     throw new RuntimeException("waitForPrompt timed out after " + (timeout/1000)
 198                             + " seconds, looking for '" + promptPattern + "', in " + lines + " lines");
 199                 }
 200                 return reply;
 201             }
 202         }
 203         // timeout
 204         logJdb(outputHandler.get());
 205         throw new RuntimeException("waitForPrompt timed out after " + (timeout/1000)
 206                 + " seconds, looking for '" + promptPattern + "', in " + lines + " lines");
 207     }
 208 
 209     public List<String> command(JdbCommand cmd) {
 210         if (!jdb.isAlive()) {
 211             if (cmd.allowExit) {
 212                 // return remaining output
 213                 return outputHandler.reset();
 214             }
 215             throw new RuntimeException("Attempt to send command '" + cmd.cmd + "' to terminated jdb");
 216         }
 217 
 218         log("> " + cmd.cmd);
 219 
 220         inputWriter.println(cmd.cmd);
 221 
 222         if (inputWriter.checkError()) {
 223             throw new RuntimeException("Unexpected IO error while writing command '" + cmd.cmd + "' to jdb stdin stream");
 224         }
 225 
 226         return waitForPrompt(1, cmd.allowExit);
 227     }
 228 
 229     public List<String> command(String cmd) {
 230         return command(new JdbCommand(cmd));
 231     }
 232 
 233     // sends "cont" command up to maxTimes until debuggee exit
 234     public void contToExit(int maxTimes) {
 235         boolean exited = false;
 236         JdbCommand cont = JdbCommand.cont().allowExit();
 237         for (int i = 0; i < maxTimes && jdb.isAlive(); i++) {
 238             String reply = command(cont).stream().collect(Collectors.joining(lineSeparator));
 239             if (reply.contains(APPLICATION_EXIT)) {
 240                 exited = true;
 241                 break;
 242             }
 243         }
 244         if (!exited && jdb.isAlive()) {
 245             throw new RuntimeException("Debuggee did not exit after " + maxTimes + " <cont> commands");
 246         }
 247     }
 248 
 249     // quits jdb by using "quit" command
 250     public void quit() {
 251         command(JdbCommand.quit());
 252     }
 253 
 254     void log(String s) {
 255         System.out.println(s);
 256     }
 257 
 258     private void logJdb(List<String> reply) {
 259         jdbOutput.addAll(reply);
 260         reply.forEach(s -> log("[jdb] " + s));
 261     }
 262 
 263     // returns the whole jdb output as a string
 264     public String getJdbOutput() {
 265         return jdbOutput.stream().collect(Collectors.joining(lineSeparator));
 266     }
 267 
 268     // handler for out/err of the pdb process
 269     private class OutputHandler extends OutputStream {
 270         // there are 2 buffers:
 271         // outStream - data from the process stdout/stderr after last get() call
 272         // cachedData - data collected at get(), cleared by reset()
 273 
 274         private final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
 275         // if the last line in the reply had EOL, the list's last element is empty
 276         private final List<String> cachedData = new ArrayList<>();
 277 
 278         @Override
 279         public synchronized void write(int b) throws IOException {
 280             outStream.write((byte)(b & 0xFF));
 281             notifyAll();
 282         }
 283         @Override
 284         public synchronized void write(byte b[], int off, int len) throws IOException {
 285             outStream.write(b, off, len);
 286             notifyAll();
 287         }
 288 
 289         // gets output after the last {@ reset}.
 290         // returned data becomes invalid after {@reset}.
 291         public synchronized List<String> get() {
 292             if (updated()) {
 293                 // we don't want to discard empty lines
 294                 String[] newLines = outStream.toString().split("\\R", -1);
 295                 if (!cachedData.isEmpty()) {
 296                     // concat the last line if previous data had no EOL
 297                     newLines[0] = cachedData.remove(cachedData.size()-1) + newLines[0];
 298                 }
 299                 cachedData.addAll(Arrays.asList(newLines));
 300                 outStream.reset();
 301             }
 302             return Collections.unmodifiableList(cachedData);
 303         }
 304 
 305         // clears last replay (does not touch replyStream)
 306         // returns list as the last get()
 307         public synchronized List<String> reset() {
 308             List<String> result = new ArrayList<>(cachedData);
 309             cachedData.clear();
 310             return result;
 311         }
 312 
 313         // tests if there are some new data after the last lastReply() call
 314         public synchronized boolean updated() {
 315             return outStream.size() > 0;
 316         }
 317     }
 318 }
 319