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