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 }