1 /* 2 * Copyright (c) 2015, 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 jdk.test.lib.apps; 25 26 import java.io.BufferedReader; 27 import java.io.ByteArrayOutputStream; 28 import java.io.IOException; 29 import java.io.InputStream; 30 import java.io.InputStreamReader; 31 import java.io.OutputStream; 32 import java.io.StringReader; 33 import java.nio.file.Files; 34 import java.nio.file.NoSuchFileException; 35 import java.nio.file.Path; 36 import java.nio.file.Paths; 37 import java.nio.file.attribute.BasicFileAttributes; 38 import java.nio.file.attribute.FileTime; 39 import java.util.ArrayList; 40 import java.util.Date; 41 import java.util.List; 42 import java.util.Map; 43 import java.util.stream.Collectors; 44 import java.util.UUID; 45 import jdk.test.lib.process.OutputBuffer; 46 import jdk.test.lib.process.ProcessTools; 47 import jdk.test.lib.process.StreamPumper; 48 49 /** 50 * This is a framework to launch an app that could be synchronized with caller 51 * to make further attach actions reliable across supported platforms 52 53 * Caller example: 54 * SmartTestApp a = SmartTestApp.startApp(cmd); 55 * // do something 56 * a.stopApp(); 57 * 58 * or fine grained control 59 * 60 * a = new SmartTestApp("MyLock.lck"); 61 * a.createLock(); 62 * a.runApp(); 63 * a.waitAppReady(); 64 * // do something 65 * a.deleteLock(); 66 * a.waitAppTerminate(); 67 * 68 * Then you can work with app output and process object 69 * 70 * output = a.getAppOutput(); 71 * process = a.getProcess(); 72 * 73 */ 74 public class LingeredApp { 75 76 private static final long spinDelay = 1000; 77 78 private long lockCreationTime; 79 private ByteArrayOutputStream stderrBuffer; 80 private ByteArrayOutputStream stdoutBuffer; 81 private Thread outPumperThread; 82 private Thread errPumperThread; 83 84 protected Process appProcess; 85 protected OutputBuffer output; 86 protected static final int appWaitTime = 100; 87 protected final String lockFileName; 88 89 /** 90 * Create LingeredApp object on caller side. Lock file have be a valid filename 91 * at writable location 92 * 93 * @param lockFileName - the name of lock file 94 */ 95 public LingeredApp(String lockFileName) { 96 this.lockFileName = lockFileName; 97 } 98 99 public LingeredApp() { 100 final String lockName = UUID.randomUUID().toString() + ".lck"; 101 this.lockFileName = lockName; 102 } 103 104 /** 105 * 106 * @return name of lock file 107 */ 108 public String getLockFileName() { 109 return this.lockFileName; 110 } 111 112 /** 113 * 114 * @return name of testapp 115 */ 116 public String getAppName() { 117 return this.getClass().getName(); 118 } 119 120 /** 121 * 122 * @return pid of java process running testapp 123 */ 124 public long getPid() { 125 if (appProcess == null) { 126 throw new RuntimeException("Process is not alive"); 127 } 128 return appProcess.pid(); 129 } 130 131 /** 132 * 133 * @return process object 134 */ 135 public Process getProcess() { 136 return appProcess; 137 } 138 139 /** 140 * 141 * @return OutputBuffer object for the LingeredApp's output. Can only be called 142 * after LingeredApp has exited. 143 */ 144 public OutputBuffer getOutput() { 145 if (appProcess.isAlive()) { 146 throw new RuntimeException("Process is still alive. Can't get its output."); 147 } 148 if (output == null) { 149 output = new OutputBuffer(stdoutBuffer.toString(), stderrBuffer.toString()); 150 } 151 return output; 152 } 153 154 /* 155 * Capture all stdout and stderr output from the LingeredApp so it can be returned 156 * to the driver app later. This code is modeled after ProcessTools.getOutput(). 157 */ 158 private void startOutputPumpers() { 159 stderrBuffer = new ByteArrayOutputStream(); 160 stdoutBuffer = new ByteArrayOutputStream(); 161 StreamPumper outPumper = new StreamPumper(appProcess.getInputStream(), stdoutBuffer); 162 StreamPumper errPumper = new StreamPumper(appProcess.getErrorStream(), stderrBuffer); 163 outPumperThread = new Thread(outPumper); 164 errPumperThread = new Thread(errPumper); 165 166 outPumperThread.setDaemon(true); 167 errPumperThread.setDaemon(true); 168 169 outPumperThread.start(); 170 errPumperThread.start(); 171 } 172 173 /** 174 * 175 * @return application output as List. Empty List if application produced no output 176 */ 177 public List<String> getAppOutput() { 178 if (appProcess.isAlive()) { 179 throw new RuntimeException("Process is still alive. Can't get its output."); 180 } 181 BufferedReader bufReader = new BufferedReader(new StringReader(output.getStdout())); 182 return bufReader.lines().collect(Collectors.toList()); 183 } 184 185 /* Make sure all part of the app use the same method to get dates, 186 as different methods could produce different results 187 */ 188 private static long epoch() { 189 return new Date().getTime(); 190 } 191 192 private static long lastModified(String fileName) throws IOException { 193 Path path = Paths.get(fileName); 194 BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class); 195 return attr.lastModifiedTime().toMillis(); 196 } 197 198 private static void setLastModified(String fileName, long newTime) throws IOException { 199 Path path = Paths.get(fileName); 200 FileTime fileTime = FileTime.fromMillis(newTime); 201 Files.setLastModifiedTime(path, fileTime); 202 } 203 204 /** 205 * create lock 206 * 207 * @throws IOException 208 */ 209 public void createLock() throws IOException { 210 Path path = Paths.get(lockFileName); 211 // Files.deleteIfExists(path); 212 Files.createFile(path); 213 lockCreationTime = lastModified(lockFileName); 214 } 215 216 /** 217 * Delete lock 218 * 219 * @throws IOException 220 */ 221 public void deleteLock() throws IOException { 222 try { 223 Path path = Paths.get(lockFileName); 224 Files.delete(path); 225 } catch (NoSuchFileException ex) { 226 // Lock already deleted. Ignore error 227 } 228 } 229 230 public void waitAppTerminate() { 231 // This code is modeled after tail end of ProcessTools.getOutput(). 232 try { 233 appProcess.waitFor(); 234 outPumperThread.join(); 235 errPumperThread.join(); 236 } catch (InterruptedException e) { 237 Thread.currentThread().interrupt(); 238 // pass 239 } 240 } 241 242 /** 243 * The app touches the lock file when it's started 244 * wait while it happens. Caller have to delete lock on wait error. 245 * 246 * @param timeout 247 * @throws java.io.IOException 248 */ 249 public void waitAppReady(long timeout) throws IOException { 250 long here = epoch(); 251 while (true) { 252 long epoch = epoch(); 253 if (epoch - here > (timeout * 1000)) { 254 throw new IOException("App waiting timeout"); 255 } 256 257 // Live process should touch lock file every second 258 long lm = lastModified(lockFileName); 259 if (lm > lockCreationTime) { 260 break; 261 } 262 263 // Make sure process didn't already exit 264 if (!appProcess.isAlive()) { 265 throw new IOException("App exited unexpectedly with " + appProcess.exitValue()); 266 } 267 268 try { 269 Thread.sleep(spinDelay); 270 } catch (InterruptedException ex) { 271 // pass 272 } 273 } 274 } 275 276 /** 277 * Analyze an environment and prepare a command line to 278 * run the app, app name should be added explicitly 279 */ 280 public List<String> runAppPrepare(List<String> vmArguments) { 281 // We should always use testjava or throw an exception, 282 // so we can't use JDKToolFinder.getJDKTool("java"); 283 // that falls back to compile java on error 284 String jdkPath = System.getProperty("test.jdk"); 285 if (jdkPath == null) { 286 // we are not under jtreg, try env 287 Map<String, String> env = System.getenv(); 288 jdkPath = env.get("TESTJAVA"); 289 } 290 291 if (jdkPath == null) { 292 throw new RuntimeException("Can't determine jdk path neither test.jdk property no TESTJAVA env are set"); 293 } 294 295 String osname = System.getProperty("os.name"); 296 String javapath = jdkPath + ((osname.startsWith("window")) ? "/bin/java.exe" : "/bin/java"); 297 298 List<String> cmd = new ArrayList<String>(); 299 cmd.add(javapath); 300 301 if (vmArguments == null) { 302 // Propagate test.vm.options to LingeredApp, filter out possible empty options 303 String testVmOpts[] = System.getProperty("test.vm.opts","").split("\\s+"); 304 for (String s : testVmOpts) { 305 if (!s.equals("")) { 306 cmd.add(s); 307 } 308 } 309 } else { 310 // Lets user manage LingeredApp options 311 cmd.addAll(vmArguments); 312 } 313 314 // Make sure we set correct classpath to run the app 315 cmd.add("-cp"); 316 String classpath = System.getProperty("test.class.path"); 317 cmd.add((classpath == null) ? "." : classpath); 318 319 return cmd; 320 } 321 322 /** 323 * Assemble command line to a printable string 324 */ 325 public void printCommandLine(List<String> cmd) { 326 // A bit of verbosity 327 StringBuilder cmdLine = new StringBuilder(); 328 for (String strCmd : cmd) { 329 cmdLine.append("'").append(strCmd).append("' "); 330 } 331 332 System.err.println("Command line: [" + cmdLine.toString() + "]"); 333 } 334 335 /** 336 * Run the app. 337 * 338 * @param vmArguments 339 * @throws IOException 340 */ 341 public void runApp(List<String> vmArguments) 342 throws IOException { 343 344 List<String> cmd = runAppPrepare(vmArguments); 345 346 cmd.add(this.getAppName()); 347 cmd.add(lockFileName); 348 349 printCommandLine(cmd); 350 351 ProcessBuilder pb = new ProcessBuilder(cmd); 352 // ProcessBuilder.start can throw IOException 353 appProcess = pb.start(); 354 355 startOutputPumpers(); 356 } 357 358 private void finishApp() { 359 OutputBuffer output = getOutput(); 360 String msg = 361 " LingeredApp stdout: [" + output.getStdout() + "];\n" + 362 " LingeredApp stderr: [" + output.getStderr() + "]\n" + 363 " LingeredApp exitValue = " + appProcess.exitValue(); 364 365 System.err.println(msg); 366 } 367 368 /** 369 * Delete lock file that signals app to terminate, then 370 * wait until app is actually terminated. 371 * @throws IOException 372 */ 373 public void stopApp() throws IOException { 374 deleteLock(); 375 // The startApp() of the derived app can throw 376 // an exception before the LA actually starts 377 if (appProcess != null) { 378 waitAppTerminate(); 379 int exitcode = appProcess.exitValue(); 380 if (exitcode != 0) { 381 throw new IOException("LingeredApp terminated with non-zero exit code " + exitcode); 382 } 383 } 384 finishApp(); 385 } 386 387 /** 388 * High level interface for test writers 389 */ 390 /** 391 * Factory method that creates LingeredApp object with ready to use application 392 * lock name is autogenerated 393 * @param cmd - vm options, could be null to auto add testvm.options 394 * @return LingeredApp object 395 * @throws IOException 396 */ 397 public static LingeredApp startApp(List<String> cmd) throws IOException { 398 LingeredApp a = new LingeredApp(); 399 a.createLock(); 400 try { 401 a.runApp(cmd); 402 a.waitAppReady(appWaitTime); 403 } catch (Exception ex) { 404 a.deleteLock(); 405 System.err.println("LingeredApp failed to start: " + ex); 406 a.finishApp(); 407 throw ex; 408 } 409 410 return a; 411 } 412 413 /** 414 * Factory method that starts pre-created LingeredApp 415 * lock name is autogenerated 416 * @param cmd - vm options, could be null to auto add testvm.options 417 * @param theApp - app to start 418 * @return LingeredApp object 419 * @throws IOException 420 */ 421 422 public static void startApp(List<String> cmd, LingeredApp theApp) throws IOException { 423 theApp.createLock(); 424 try { 425 theApp.runApp(cmd); 426 theApp.waitAppReady(appWaitTime); 427 } catch (Exception ex) { 428 theApp.deleteLock(); 429 throw ex; 430 } 431 } 432 433 public static LingeredApp startApp() throws IOException { 434 return startApp(null); 435 } 436 437 public static void stopApp(LingeredApp app) throws IOException { 438 if (app != null) { 439 // LingeredApp can throw an exception during the intialization, 440 // make sure we don't have cascade NPE 441 app.stopApp(); 442 } 443 } 444 445 /** 446 * LastModified time might not work correctly in some cases it might 447 * cause later failures 448 */ 449 450 public static boolean isLastModifiedWorking() { 451 boolean sane = true; 452 try { 453 long lm = lastModified("."); 454 if (lm == 0) { 455 System.err.println("SANITY Warning! The lastModifiedTime() doesn't work on this system, it returns 0"); 456 sane = false; 457 } 458 459 long now = epoch(); 460 if (lm > now) { 461 System.err.println("SANITY Warning! The Clock is wrong on this system lastModifiedTime() > getTime()"); 462 sane = false; 463 } 464 465 setLastModified(".", epoch()); 466 long lm1 = lastModified("."); 467 if (lm1 <= lm) { 468 System.err.println("SANITY Warning! The setLastModified doesn't work on this system"); 469 sane = false; 470 } 471 } 472 catch(IOException e) { 473 System.err.println("SANITY Warning! IOException during sanity check " + e); 474 sane = false; 475 } 476 477 return sane; 478 } 479 480 /** 481 * This part is the application it self 482 */ 483 public static void main(String args[]) { 484 485 if (args.length != 1) { 486 System.err.println("Lock file name is not specified"); 487 System.exit(7); 488 } 489 490 String theLockFileName = args[0]; 491 492 try { 493 Path path = Paths.get(theLockFileName); 494 495 while (Files.exists(path)) { 496 // Touch the lock to indicate our readiness 497 setLastModified(theLockFileName, epoch()); 498 Thread.sleep(spinDelay); 499 } 500 } catch (NoSuchFileException ex) { 501 // Lock deleted while we are setting last modified time. 502 // Ignore error and lets the app exits 503 } catch (Exception ex) { 504 System.err.println("LingeredApp ERROR: " + ex); 505 // Leave exit_code = 1 to Java launcher 506 System.exit(3); 507 } 508 509 System.exit(0); 510 } 511 }