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 cmd.replyIsSimplePrompt ? waitForSimplePrompt(1, cmd.allowExit) : 227 waitForPrompt(1, cmd.allowExit); 228 } 229 230 public List<String> command(String cmd) { 231 return command(new JdbCommand(cmd)); 232 } 233 234 // sends "cont" command up to maxTimes until debuggee exit 235 public void contToExit(int maxTimes) { 236 boolean exited = false; 237 JdbCommand cont = JdbCommand.cont().allowExit(); 238 for (int i = 0; i < maxTimes && jdb.isAlive(); i++) { 239 String reply = command(cont).stream().collect(Collectors.joining(lineSeparator)); 240 if (reply.contains(APPLICATION_EXIT)) { 241 exited = true; 242 break; 243 } 244 } 245 if (!exited && jdb.isAlive()) { 246 throw new RuntimeException("Debuggee did not exit after " + maxTimes + " <cont> commands"); 247 } 248 } 249 250 // quits jdb by using "quit" command 251 public void quit() { 252 command(JdbCommand.quit()); 253 } 254 255 void log(String s) { 256 System.out.println(s); 257 } 258 259 private void logJdb(List<String> reply) { 260 jdbOutput.addAll(reply); 261 reply.forEach(s -> log("[jdb] " + s)); 262 } 263 264 // returns the whole jdb output as a string 265 public String getJdbOutput() { 266 return jdbOutput.stream().collect(Collectors.joining(lineSeparator)); 267 } 268 269 // handler for out/err of the pdb process 270 private class OutputHandler extends OutputStream { 271 // there are 2 buffers: 272 // outStream - data from the process stdout/stderr after last get() call 273 // cachedData - data collected at get(), cleared by reset() 274 275 private final ByteArrayOutputStream outStream = new ByteArrayOutputStream(); 276 // if the last line in the reply had EOL, the list's last element is empty 277 private final List<String> cachedData = new ArrayList<>(); 278 279 @Override 280 public synchronized void write(int b) throws IOException { 281 outStream.write((byte)(b & 0xFF)); 282 notifyAll(); 283 } 284 @Override 285 public synchronized void write(byte b[], int off, int len) throws IOException { 286 outStream.write(b, off, len); 287 notifyAll(); 288 } 289 290 // gets output after the last {@ reset}. 291 // returned data becomes invalid after {@reset}. 292 public synchronized List<String> get() { 293 if (updated()) { 294 // we don't want to discard empty lines 295 String[] newLines = outStream.toString().split("\\R", -1); 296 if (!cachedData.isEmpty()) { 297 // concat the last line if previous data had no EOL 298 newLines[0] = cachedData.remove(cachedData.size()-1) + newLines[0]; 299 } 300 cachedData.addAll(Arrays.asList(newLines)); 301 outStream.reset(); 302 } 303 return Collections.unmodifiableList(cachedData); 304 } 305 306 // clears last replay (does not touch replyStream) 307 // returns list as the last get() 308 public synchronized List<String> reset() { 309 List<String> result = new ArrayList<>(cachedData); 310 cachedData.clear(); 311 return result; 312 } 313 314 // tests if there are some new data after the last lastReply() call 315 public synchronized boolean updated() { 316 return outStream.size() > 0; 317 } 318 } 319 } 320