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