1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 /** 20 * @author Vitaly A. Provodin 21 */ 22 23 package org.apache.harmony.jpda.tests.jdwp.share; 24 25 import java.lang.reflect.Field; 26 27 import java.io.*; 28 import java.nio.file.*; 29 30 import java.io.IOException; 31 import java.util.Arrays; 32 import java.util.ArrayList; 33 import java.util.List; 34 import java.util.Vector; 35 36 import org.apache.harmony.jpda.tests.framework.LogWriter; 37 import org.apache.harmony.jpda.tests.framework.StreamRedirector; 38 import org.apache.harmony.jpda.tests.framework.TestErrorException; 39 import org.apache.harmony.jpda.tests.framework.jdwp.JDWPDebuggeeWrapper; 40 import org.apache.harmony.jpda.tests.share.JPDATestOptions; 41 42 /** 43 * This class provides basic DebuggeeWrapper implementation based on JUnit framework, 44 * which can launch and control debuggee process. 45 */ 46 public class JDWPUnitDebuggeeProcessWrapper extends JDWPDebuggeeWrapper { 47 48 /** 49 * Target VM debuggee process. 50 */ 51 public Process process; 52 53 protected StreamRedirector errRedir; 54 protected StreamRedirector outRedir; 55 56 /** 57 * The expected exit code for the debuggee process. 58 */ 59 private int expectedExitCode = 0; 60 61 /** 62 * Creates new instance with given data. 63 * 64 * @param settings 65 * test run options 66 * @param logWriter 67 * where to print log messages 68 */ 69 public JDWPUnitDebuggeeProcessWrapper(JPDATestOptions settings, LogWriter logWriter) { 70 super(settings, logWriter); 71 } 72 73 /** 74 * Sets the expected exit code. This is meant to be used by tests that will request target 75 * VM termination with VirtualMachine.Exit command. 76 */ 77 public void setExpectedExitCode(int expectedExitCode) { 78 this.expectedExitCode = expectedExitCode; 79 } 80 81 /** 82 * Launches process and redirects output. 83 */ 84 public void launchProcessAndRedirectors(String cmdLine) throws IOException { 85 logWriter.println("Launch process: " + cmdLine); 86 process = launchProcess(cmdLine); 87 logWriter.println("Launched process"); 88 if (process != null) { 89 logWriter.println("Start redirectors"); 90 errRedir = new StreamRedirector(process.getErrorStream(), logWriter, "STDERR"); 91 errRedir.setDaemon(true); 92 errRedir.start(); 93 outRedir = new StreamRedirector(process.getInputStream(), logWriter, "STDOUT"); 94 outRedir.setDaemon(true); 95 outRedir.start(); 96 logWriter.println("Started redirectors"); 97 } 98 } 99 100 /** 101 * Waits for process to exit and closes output redirectors 102 */ 103 public void finishProcessAndRedirectors() { 104 if (process != null) { 105 try { 106 logWriter.println("Waiting for process exit"); 107 WaitForProcessExit(process); 108 logWriter.println("Finished process"); 109 } catch (IOException e) { 110 throw new TestErrorException("IOException in waiting for process exit: ", e); 111 } 112 113 logWriter.println("Waiting for redirectors finish"); 114 if (outRedir != null) { 115 outRedir.exit(); 116 try { 117 outRedir.join(settings.getTimeout()); 118 } catch (InterruptedException e) { 119 logWriter.println("InterruptedException in stopping outRedirector: " + e); 120 } 121 if (outRedir.isAlive()) { 122 logWriter.println("WARNING: redirector not stopped: " + outRedir.getName()); 123 } 124 } 125 if (errRedir != null) { 126 errRedir.exit(); 127 try { 128 errRedir.join(settings.getTimeout()); 129 } catch (InterruptedException e) { 130 logWriter.println("InterruptedException in stopping errRedirector: " + e); 131 } 132 if (errRedir.isAlive()) { 133 logWriter.println("WARNING: redirector not stopped: " + errRedir.getName()); 134 } 135 } 136 logWriter.println("Finished redirectors"); 137 } 138 } 139 140 /** 141 * Launches process with given command line. 142 * 143 * @param cmdLine 144 * command line 145 * @return associated Process object or null if not available 146 * @throws IOException 147 * if error occurred in launching process 148 */ 149 protected Process launchProcess(String cmdLine) throws IOException { 150 151 // Runtime.exec(String) does not preserve quoted arguments 152 // process = Runtime.getRuntime().exec(cmdLine); 153 154 String args[] = splitCommandLine(cmdLine); 155 process = Runtime.getRuntime().exec(args); 156 return process; 157 } 158 159 /** 160 * Splits command line into arguments preserving spaces in quoted arguments 161 * either with single and double quotes (not prefixed by '\'). 162 * 163 * @param cmd 164 * command line 165 * @return associated Process object or null if not available 166 */ 167 /* 168 public String[] splitCommandLine(String cmd) { 169 170 // allocate array for parsed arguments 171 int max_argc = 250; 172 Vector argv = new Vector(); 173 174 // parse command line 175 int len = cmd.length(); 176 if (len > 0) { 177 for (int arg = 0; arg < len;) { 178 // skip initial spaces 179 while (Character.isWhitespace(cmd.charAt(arg))) arg++; 180 // parse non-spaced or quoted argument 181 for (int p = arg; ; p++) { 182 // check for ending separator 183 if (p >= len || Character.isWhitespace(cmd.charAt(p))) { 184 if (p > len) p = len; 185 String val = cmd.substring(arg, p); 186 argv.add(val); 187 arg = p + 1; 188 break; 189 } 190 191 // check for starting quote 192 if (cmd.charAt(p) == '\"') { 193 char quote = cmd.charAt(p++); 194 // skip all chars until terminating quote or end of line 195 for (; p < len; p++) { 196 // check for terminating quote 197 if (cmd.charAt(p) == quote) 198 break; 199 // skip escaped quote 200 if (cmd.charAt(p) == '\\' && (p+1) < len && cmd.charAt(p+1) == quote) 201 p++; 202 } 203 } 204 205 // skip escaped quote 206 if (cmd.charAt(p) == '\\' && (p+1) < len && cmd.charAt(p+1) == '\"') { 207 p++; 208 } 209 } 210 } 211 } 212 213 logWriter.println("Splitted command line: " + argv); 214 int size = argv.size(); 215 String args[] = new String[size]; 216 return (String[])argv.toArray(args); 217 } 218 */ 219 public String[] splitCommandLine(String cmd) { 220 221 int len = cmd.length(); 222 char chars[] = new char[len]; 223 Vector<String> argv = new Vector<String>(); 224 225 if (len > 0) { 226 for (int arg = 0; arg < len;) { 227 // skip initial spaces 228 while (Character.isWhitespace(cmd.charAt(arg))) arg++; 229 // parse non-spaced or quoted argument 230 for (int p = arg, i = 0; ; p++) { 231 // check for starting quote 232 if (p < len && (cmd.charAt(p) == '\"' || cmd.charAt(p) == '\'')) { 233 char quote = cmd.charAt(p++); 234 // copy all chars until terminating quote or end of line 235 for (; p < len; p++) { 236 // check for terminating quote 237 if (cmd.charAt(p) == quote) { 238 p++; 239 break; 240 } 241 // preserve escaped quote 242 if (cmd.charAt(p) == '\\' && (p+1) < len && cmd.charAt(p+1) == quote) 243 p++; 244 chars[i++] = cmd.charAt(p); 245 } 246 } 247 248 // check for ending separator 249 if (p >= len || Character.isWhitespace(cmd.charAt(p))) { 250 String val = new String(chars, 0, i); 251 argv.add(val); 252 arg = p + 1; 253 break; 254 } 255 256 // preserve escaped quote 257 if (cmd.charAt(p) == '\\' && (p+1) < len 258 && (cmd.charAt(p+1) == '\"' || cmd.charAt(p+1) == '\'')) { 259 p++; 260 } 261 262 // copy current char 263 chars[i++] = cmd.charAt(p); 264 } 265 } 266 } 267 268 logWriter.println("Splitted command line: " + argv); 269 int size = argv.size(); 270 String args[] = new String[size]; 271 return argv.toArray(args); 272 } 273 274 /** 275 * Waits for launched process to exit. 276 * 277 * @param process 278 * associated Process object or null if not available 279 * @throws IOException 280 * if any exception occurs in waiting 281 */ 282 protected void WaitForProcessExit(Process process) throws IOException { 283 ProcessWaiter thrd = new ProcessWaiter(); 284 thrd.setDaemon(true); 285 thrd.start(); 286 try { 287 thrd.join(settings.getTimeout()); 288 } catch (InterruptedException e) { 289 throw new TestErrorException(e); 290 } 291 292 if (thrd.isAlive()) { 293 // ProcessWaiter thread is still running (after we wait until a timeout) but is 294 // waiting for the debuggee process to exit. We send an interrupt request to 295 // that thread so it receives an InterrupedException and terminates. 296 thrd.interrupt(); 297 } 298 299 try { 300 int exitCode = process.exitValue(); 301 logWriter.println("Finished debuggee with exit code: " + exitCode); 302 if (exitCode != expectedExitCode) { 303 throw new TestErrorException("Debuggee exited with code " + exitCode + 304 " but we expected code " + expectedExitCode); 305 } 306 } catch (IllegalThreadStateException e) { 307 logWriter.println("Terminate debuggee process with " + e); 308 GetRemoteStackTrace(process); 309 throw new TestErrorException("Debuggee process did not finish during timeout", e); 310 } finally { 311 // dispose any resources of the process 312 process.destroy(); 313 } 314 } 315 316 private int GetRealPid(Process process) { 317 int initial_pid = GetInitialPid(process); 318 logWriter.println("initial pid: " + initial_pid); 319 String[] names = new String[] { "java", "dalvikvm", "dalvikvm32", "dalvikvm64" }; 320 return FindPidFor(Paths.get("/proc").resolve(Integer.toString(initial_pid)), names); 321 } 322 323 private int getPid(Path proc) throws IOException { 324 try { 325 // See man 5 proc for information on stat file. All we really care about is that it is a 326 // list of various pieces of information about the process separated by ' '. The first of 327 // these is the PID. 328 return Integer.valueOf(new String(Files.readAllBytes(proc.resolve("stat"))).split(" ")[0]); 329 } catch (IOException e) { 330 return -1; 331 } 332 } 333 334 private int FindPidFor(Path cur, String[] names) { 335 try { 336 if (!cur.toFile().exists()) { 337 return -1; 338 } 339 int pid = getPid(cur); 340 if (Arrays.stream(names).anyMatch( 341 (x) -> { 342 try { 343 return cur.resolve("exe").toRealPath().endsWith(x); 344 } catch (Exception e) { 345 return false; 346 } 347 })) { 348 return pid; 349 } else { 350 Path children = cur.resolve("task").resolve(Integer.toString(pid)).resolve("children"); 351 if (!children.toFile().exists()) { 352 return -1; 353 } else { 354 for (String child_pid : ReadChildPids(children.toFile())) { 355 logWriter.println("Examining pid: " + child_pid); 356 int res = FindPidFor(Paths.get("/proc").resolve(child_pid), names); 357 if (res != -1) { 358 return res; 359 } 360 } 361 return -1; 362 } 363 } 364 } catch (Exception e) { 365 return -1; 366 } 367 } 368 369 private String[] ReadChildPids(File children) throws Exception { 370 if (!children.exists()) { 371 return new String[0]; 372 } else { 373 return new String(Files.readAllBytes(children.toPath())).split(" "); 374 } 375 } 376 377 private int GetInitialPid(Process process) { 378 try { 379 Class<?> unix_process_class = Class.forName("java.lang.UNIXProcess"); 380 if (!process.getClass().equals(unix_process_class)) { 381 throw new TestErrorException("Process is of unexpected type. Timeout happened."); 382 } 383 Field pid = unix_process_class.getDeclaredField("pid"); 384 pid.setAccessible(true); 385 return (int)pid.get(process); 386 } catch (ReflectiveOperationException e) { 387 throw new TestErrorException("Unable to get pid from process to debug timeout!", e); 388 } 389 } 390 391 private void GetRemoteStackTrace(Process process) throws IOException { 392 String pid = Integer.toString(GetRealPid(process)); 393 logWriter.println("trying to dump " + pid); 394 List<String> cmd = new ArrayList<>(Arrays.asList(splitCommandLine(settings.getDumpProcessCommand()))); 395 cmd.add(pid); 396 logWriter.println("command: " + cmd); 397 398 ProcessBuilder b = new ProcessBuilder(cmd).redirectErrorStream(true); 399 Process p = null; 400 StreamRedirector out = null; 401 try { 402 p = b.start(); 403 out = new StreamRedirector(p.getInputStream(), logWriter, "dump-" + pid); 404 out.setDaemon(true); 405 out.start(); 406 p.waitFor(); 407 // Wait 5 seconds for the streams to catch up. 408 Thread.sleep(5000); 409 } catch (Exception e) { 410 throw new TestErrorException("Unable to get process dump!", e); 411 } finally { 412 if (p != null) { 413 p.destroy(); 414 } 415 if (out != null) { 416 try { 417 out.exit(); 418 out.join(); 419 } catch (Exception e) { 420 logWriter.println("Suppressing error: " + e); 421 } 422 } 423 } 424 } 425 426 /** 427 * Separate thread for waiting for process exit for specified timeout. 428 */ 429 class ProcessWaiter extends Thread { 430 @Override 431 public void run() { 432 try { 433 process.waitFor(); 434 } catch (InterruptedException e) { 435 logWriter.println("Ignoring exception in ProcessWaiter thread interrupted: " + e); 436 } 437 } 438 } 439 440 @Override 441 public void start() { 442 } 443 444 @Override 445 public void stop() { 446 } 447 } 448