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