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