Coverage Report - com.jcabi.log.VerboseProcess
 
Classes in this File Line Coverage Branch Coverage Complexity
VerboseProcess
0%
0/82
0%
0/32
2.526
VerboseProcess$Monitor
0%
0/30
0%
0/4
2.526
VerboseProcess$Result
0%
0/8
N/A
2.526
 
 1  
 /**
 2  
  * Copyright (c) 2012-2015, jcabi.com
 3  
  * All rights reserved.
 4  
  *
 5  
  * Redistribution and use in source and binary forms, with or without
 6  
  * modification, are permitted provided that the following conditions
 7  
  * are met: 1) Redistributions of source code must retain the above
 8  
  * copyright notice, this list of conditions and the following
 9  
  * disclaimer. 2) Redistributions in binary form must reproduce the above
 10  
  * copyright notice, this list of conditions and the following
 11  
  * disclaimer in the documentation and/or other materials provided
 12  
  * with the distribution. 3) Neither the name of the jcabi.com nor
 13  
  * the names of its contributors may be used to endorse or promote
 14  
  * products derived from this software without specific prior written
 15  
  * permission.
 16  
  *
 17  
  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 18  
  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
 19  
  * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 20  
  * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 21  
  * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 22  
  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 23  
  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 24  
  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 25  
  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 26  
  * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 27  
  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 28  
  * OF THE POSSIBILITY OF SUCH DAMAGE.
 29  
  */
 30  
 package com.jcabi.log;
 31  
 
 32  
 import java.io.BufferedReader;
 33  
 import java.io.BufferedWriter;
 34  
 import java.io.ByteArrayOutputStream;
 35  
 import java.io.Closeable;
 36  
 import java.io.IOException;
 37  
 import java.io.InputStream;
 38  
 import java.io.OutputStream;
 39  
 import java.io.OutputStreamWriter;
 40  
 import java.io.UnsupportedEncodingException;
 41  
 import java.nio.channels.Channels;
 42  
 import java.nio.channels.ClosedByInterruptException;
 43  
 import java.util.concurrent.Callable;
 44  
 import java.util.concurrent.CountDownLatch;
 45  
 import java.util.concurrent.TimeUnit;
 46  
 import java.util.logging.Level;
 47  
 import lombok.EqualsAndHashCode;
 48  
 import lombok.ToString;
 49  
 
 50  
 /**
 51  
  * Utility class for getting {@code stdout} from a running process
 52  
  * and logging it through SLF4J.
 53  
  *
 54  
  * <p>For example:
 55  
  *
 56  
  * <pre> String name = new VerboseProcess(
 57  
  *   new ProcessBuilder("who", "am", "i")
 58  
  * ).stdout();</pre>
 59  
  *
 60  
  * <p>The class throws an exception if the process returns a non-zero exit
 61  
  * code.
 62  
  *
 63  
  * <p>The class is thread-safe.
 64  
  *
 65  
  * @author Yegor Bugayenko (yegor@teamed.io)
 66  
  * @version $Id: 2fe764d75cf328534f0c9e88fee3405e9e0d6a10 $
 67  
  * @since 0.5
 68  
  */
 69  0
 @ToString
 70  0
 @EqualsAndHashCode(of = "process")
 71  
 @SuppressWarnings({ "PMD.DoNotUseThreads", "PMD.TooManyMethods" })
 72  0
 public final class VerboseProcess implements Closeable {
 73  
 
 74  
     /**
 75  
      * Charset.
 76  
      */
 77  
     private static final String UTF_8 = "UTF-8";
 78  
 
 79  
     /**
 80  
      * Number of stream monitors.
 81  
      */
 82  
     private static final int N_MONITORS = 2;
 83  
 
 84  
     /**
 85  
      * The process we're working with.
 86  
      */
 87  
     private final transient Process process;
 88  
 
 89  
     /**
 90  
      * Log level for stdout.
 91  
      */
 92  
     private final transient Level olevel;
 93  
 
 94  
     /**
 95  
      * Log level for stderr.
 96  
      */
 97  
     private final transient Level elevel;
 98  
 
 99  
     /**
 100  
      * Stream monitors.
 101  
      */
 102  0
     private final transient Thread[] monitors = new Thread[N_MONITORS];
 103  
 
 104  
     /**
 105  
      * Flag to indicate the closure of this process.
 106  
      */
 107  
     private transient boolean closed;
 108  
 
 109  
     /**
 110  
      * Public ctor.
 111  
      * @param prc The process to work with
 112  
      */
 113  
     public VerboseProcess(final Process prc) {
 114  0
         this(prc, Level.INFO, Level.WARNING);
 115  0
     }
 116  
 
 117  
     /**
 118  
      * Public ctor (builder will be configured to redirect error input to
 119  
      * the {@code stdout} and will receive an empty {@code stdin}).
 120  
      * @param builder Process builder to work with
 121  
      */
 122  
     public VerboseProcess(final ProcessBuilder builder) {
 123  0
         this(VerboseProcess.start(builder));
 124  0
     }
 125  
 
 126  
     /**
 127  
      * Public ctor, with a given process and logging levels for {@code stdout}
 128  
      * and {@code stderr}.
 129  
      * @param prc Process to execute and monitor
 130  
      * @param stdout Log level for stdout
 131  
      * @param stderr Log level for stderr
 132  
      * @since 0.11
 133  
      */
 134  
     public VerboseProcess(final Process prc, final Level stdout,
 135  0
         final Level stderr) {
 136  0
         if (prc == null) {
 137  0
             throw new IllegalArgumentException("process can't be NULL");
 138  
         }
 139  0
         if (stdout == null) {
 140  0
             throw new IllegalArgumentException("stdout LEVEL can't be NULL");
 141  
         }
 142  0
         if (stderr == null) {
 143  0
             throw new IllegalArgumentException("stderr LEVEL can't be NULL");
 144  
         }
 145  0
         this.process = prc;
 146  0
         this.olevel = stdout;
 147  0
         this.elevel = stderr;
 148  0
     }
 149  
 
 150  
     /**
 151  
      * Public ctor, with a given process and logging levels for {@code stdout}
 152  
      * and {@code stderr}.
 153  
      * @param bdr Process builder to execute and monitor
 154  
      * @param stdout Log level for stdout
 155  
      * @param stderr Log level for stderr
 156  
      * @since 0.12
 157  
      */
 158  
     public VerboseProcess(final ProcessBuilder bdr, final Level stdout,
 159  
         final Level stderr) {
 160  0
         this(VerboseProcess.start(bdr), stdout, stderr);
 161  0
     }
 162  
 
 163  
     /**
 164  
      * Get {@code stdout} from the process, after its finish (the method will
 165  
      * wait for the process and log its output).
 166  
      *
 167  
      * <p>The method will check process exit code, and if it won't be equal
 168  
      * to zero a runtime exception will be thrown. A non-zero exit code
 169  
      * usually is an indicator of problem. If you want to ignore this code,
 170  
      * use {@link #stdoutQuietly()} instead.
 171  
      *
 172  
      * @return Full {@code stdout} of the process
 173  
      */
 174  
     public String stdout() {
 175  0
         return this.stdout(true);
 176  
     }
 177  
 
 178  
     /**
 179  
      * Get {@code stdout} from the process, after its finish (the method will
 180  
      * wait for the process and log its output).
 181  
      *
 182  
      * <p>This method ignores exit code of the process. Even if it is
 183  
      * not equal to zero (which usually is an indicator of an error), the
 184  
      * method will quietly return its output. The method is useful when
 185  
      * you're running a background process. You will kill it with
 186  
      * {@link Process#destroy()}, which usually will lead to a non-zero
 187  
      * exit code, which you want to ignore.
 188  
      *
 189  
      * @return Full {@code stdout} of the process
 190  
      * @since 0.10
 191  
      */
 192  
     public String stdoutQuietly() {
 193  0
         return this.stdout(false);
 194  
     }
 195  
 
 196  
     /**
 197  
      * Wait for the process to stop, logging its output in parallel.
 198  
      * @return Stdout produced by the process
 199  
      * @throws InterruptedException If interrupted in between
 200  
      */
 201  
     public Result waitFor() throws InterruptedException {
 202  0
         final CountDownLatch done = new CountDownLatch(N_MONITORS);
 203  0
         final ByteArrayOutputStream stdout = new ByteArrayOutputStream();
 204  0
         final ByteArrayOutputStream stderr = new ByteArrayOutputStream();
 205  0
         this.launchMonitors(done, stdout, stderr);
 206  0
         int code = 0;
 207  
         try {
 208  0
             code = this.process.waitFor();
 209  
         } finally {
 210  0
             Logger.debug(
 211  
                 this,
 212  
                 "#waitFor(): process finished: %s",
 213  
                 this.process
 214  
             );
 215  0
             if (!done.await(2L, TimeUnit.SECONDS)) {
 216  0
                 Logger.error(this, "#wait() failed");
 217  
             }
 218  
         }
 219  
         try {
 220  0
             return new Result(
 221  
                 code,
 222  
                 stdout.toString(VerboseProcess.UTF_8),
 223  
                 stderr.toString(VerboseProcess.UTF_8)
 224  
             );
 225  0
         } catch (final UnsupportedEncodingException ex) {
 226  0
             throw new IllegalStateException(ex);
 227  
         }
 228  
     }
 229  
 
 230  
     @Override
 231  
     public void close() {
 232  0
         synchronized (this.monitors) {
 233  0
             this.closed = true;
 234  0
         }
 235  0
         for (final Thread monitor : this.monitors) {
 236  0
             if (monitor != null) {
 237  0
                 monitor.interrupt();
 238  0
                 Logger.debug(this, "monitor interrupted");
 239  
             }
 240  
         }
 241  0
         this.process.destroy();
 242  0
         Logger.debug(this, "underlying process destroyed");
 243  0
     }
 244  
 
 245  
     /**
 246  
      * Start a process from the given builder.
 247  
      * @param builder Process builder to work with
 248  
      * @return Process started
 249  
      */
 250  
     private static Process start(final ProcessBuilder builder) {
 251  0
         if (builder == null) {
 252  0
             throw new IllegalArgumentException("builder can't be NULL");
 253  
         }
 254  
         try {
 255  0
             final Process process = builder.start();
 256  0
             process.getOutputStream().close();
 257  0
             return process;
 258  0
         } catch (final IOException ex) {
 259  0
             throw new IllegalStateException(ex);
 260  
         }
 261  
     }
 262  
 
 263  
     /**
 264  
      * Get standard output and check for non-zero exit code (if required).
 265  
      * @param check TRUE if we should check for non-zero exit code
 266  
      * @return Full {@code stdout} of the process
 267  
      */
 268  
     @SuppressWarnings("PMD.PrematureDeclaration")
 269  
     private String stdout(final boolean check) {
 270  0
         final long start = System.currentTimeMillis();
 271  
         final Result result;
 272  
         try {
 273  0
             result = this.waitFor();
 274  0
         } catch (final InterruptedException ex) {
 275  0
             Thread.currentThread().interrupt();
 276  0
             throw new IllegalStateException(ex);
 277  0
         }
 278  0
         Logger.debug(
 279  
             this,
 280  
             "#stdout(): process %s completed (code=%d, size=%d) in %[ms]s",
 281  
             this.process, result.code(), result.stdout().length(),
 282  
             System.currentTimeMillis() - start
 283  
         );
 284  0
         if (check && result.code() != 0) {
 285  0
             throw new IllegalArgumentException(
 286  
                 Logger.format(
 287  
                     "Non-zero exit code %d: %[text]s",
 288  
                     result.code(),
 289  
                     result.stdout()
 290  
                 )
 291  
             );
 292  
         }
 293  0
         return result.stdout();
 294  
     }
 295  
 
 296  
     /**
 297  
      * Launch monitors for the underlying process.
 298  
      * @param done Latch that signals termination of all monitors
 299  
      * @param stdout Stream to write the underlying process's output
 300  
      * @param stderr Stream to wrint the underlying process's error output
 301  
      */
 302  
     private void launchMonitors(
 303  
         final CountDownLatch done,
 304  
         final ByteArrayOutputStream stdout,
 305  
         final ByteArrayOutputStream stderr) {
 306  0
         synchronized (this.monitors) {
 307  0
             if (this.closed) {
 308  0
                 done.countDown();
 309  0
                 done.countDown();
 310  
             } else {
 311  0
                 this.monitors[0] = this.monitor(
 312  
                     this.process.getInputStream(),
 313  
                     done,
 314  
                     stdout,
 315  
                     this.olevel,
 316  
                     "out"
 317  
                 );
 318  0
                 Logger.debug(
 319  
                     this,
 320  
                     "#waitFor(): waiting for stdout of %s in %s...",
 321  
                     this.process,
 322  
                     this.monitors[0]
 323  
                 );
 324  0
                 this.monitors[1] = this.monitor(
 325  
                     this.process.getErrorStream(),
 326  
                     done,
 327  
                     stderr,
 328  
                     this.elevel,
 329  
                     "err"
 330  
                 );
 331  0
                 Logger.debug(
 332  
                     this,
 333  
                     "#waitFor(): waiting for stderr of %s in %s...",
 334  
                     this.process,
 335  
                     this.monitors[1]
 336  
                 );
 337  
             }
 338  0
         }
 339  0
     }
 340  
 
 341  
     /**
 342  
      * Monitor this input input.
 343  
      * @param input Stream to monitor
 344  
      * @param done Count down latch to signal when done
 345  
      * @param output Buffer to write to
 346  
      * @param level Logging level
 347  
      * @param name Name of this monitor. Used in logging as part of threadname
 348  
      * @return Thread which is monitoring
 349  
      * @checkstyle ParameterNumber (6 lines)
 350  
      */
 351  
     private Thread monitor(final InputStream input,
 352  
         final CountDownLatch done,
 353  
         final OutputStream output, final Level level, final String name) {
 354  0
         final Thread thread = new Thread(
 355  
             new VerboseRunnable(
 356  
                 new VerboseProcess.Monitor(input, done, output, level),
 357  
                 false
 358  
             )
 359  
         );
 360  0
         thread.setName(
 361  
             String.format(
 362  
                 "VrbPrc.Monitor-%d-%s",
 363  
                 this.hashCode(),
 364  
                 name
 365  
             )
 366  
         );
 367  0
         thread.setDaemon(true);
 368  0
         thread.start();
 369  0
         return thread;
 370  
     }
 371  
 
 372  
     /**
 373  
      * Close quietly.
 374  
      * @param res Resource to close
 375  
      */
 376  
     private static void close(final Closeable res) {
 377  
         try {
 378  0
             res.close();
 379  0
         } catch (final IOException ex) {
 380  0
             Logger.error(
 381  
                 VerboseProcess.class,
 382  
                 "failed to close resource: %[exception]s",
 383  
                 ex
 384  
             );
 385  0
         }
 386  0
     }
 387  
 
 388  
     /**
 389  
      * Stream monitor.
 390  
      */
 391  0
     private static final class Monitor implements Callable<Void> {
 392  
         /**
 393  
          * Stream to read.
 394  
          */
 395  
         private final transient InputStream input;
 396  
         /**
 397  
          * Latch to count down when done.
 398  
          */
 399  
         private final transient CountDownLatch done;
 400  
         /**
 401  
          * Buffer to save output.
 402  
          */
 403  
         private final transient OutputStream output;
 404  
         /**
 405  
          * Log level.
 406  
          */
 407  
         private final transient Level level;
 408  
         /**
 409  
          * Ctor.
 410  
          * @param inp Stream to monitor
 411  
          * @param latch Count down latch to signal when done
 412  
          * @param out Buffer to write to
 413  
          * @param lvl Logging level
 414  
          * @checkstyle ParameterNumber (5 lines)
 415  
          */
 416  
         Monitor(final InputStream inp, final CountDownLatch latch,
 417  0
             final OutputStream out, final Level lvl) {
 418  0
             this.input = inp;
 419  0
             this.done = latch;
 420  0
             this.output = out;
 421  0
             this.level = lvl;
 422  0
         }
 423  
         @Override
 424  
         public Void call() throws Exception {
 425  0
             final BufferedReader reader = new BufferedReader(
 426  
                 Channels.newReader(
 427  
                     Channels.newChannel(this.input),
 428  
                     VerboseProcess.UTF_8
 429  
                 )
 430  
             );
 431  
             try {
 432  0
                 final BufferedWriter writer = new BufferedWriter(
 433  
                     new OutputStreamWriter(this.output, VerboseProcess.UTF_8)
 434  
                 );
 435  
                 try {
 436  
                     while (true) {
 437  0
                         if (Thread.interrupted()) {
 438  0
                             Logger.debug(
 439  
                                 VerboseProcess.class,
 440  
                                 "explicitly interrupting read from buffer"
 441  
                             );
 442  0
                             break;
 443  
                         }
 444  0
                         final String line = reader.readLine();
 445  0
                         if (line == null) {
 446  0
                             break;
 447  
                         }
 448  0
                         Logger.log(
 449  
                             this.level, VerboseProcess.class,
 450  
                             ">> %s", line
 451  
                         );
 452  0
                         writer.write(line);
 453  0
                         writer.newLine();
 454  0
                     }
 455  0
                 } catch (final ClosedByInterruptException ex) {
 456  0
                     Thread.interrupted();
 457  0
                     Logger.debug(
 458  
                         VerboseProcess.class,
 459  
                         "Monitor is interrupted in the expected way"
 460  
                     );
 461  0
                 } catch (final IOException ex) {
 462  0
                     Logger.error(
 463  
                         VerboseProcess.class,
 464  
                         "Error reading from process stream: %[exception]s",
 465  
                         ex
 466  
                     );
 467  
                 } finally {
 468  0
                     VerboseProcess.close(writer);
 469  0
                     this.done.countDown();
 470  0
                 }
 471  
             } finally {
 472  0
                 VerboseProcess.close(reader);
 473  0
             }
 474  0
             return null;
 475  
         }
 476  
     }
 477  
 
 478  
     /**
 479  
      * Class representing the result of a process.
 480  
      */
 481  
     public static final class Result {
 482  
 
 483  
         /**
 484  
          * Returned code from the process.
 485  
          */
 486  
         private final transient int exit;
 487  
 
 488  
         /**
 489  
          * {@code stdout} from the process.
 490  
          */
 491  
         private final transient String out;
 492  
 
 493  
         /**
 494  
          * {@code stderr} from the process.
 495  
          */
 496  
         private final transient String err;
 497  
 
 498  
         /**
 499  
          * Result class constructor.
 500  
          * @param code The exit code.
 501  
          * @param stdout The {@code stdout} from the process.
 502  
          * @param stderr The {@code stderr} from the process.
 503  
          */
 504  0
         Result(final int code, final String stdout, final String stderr) {
 505  0
             this.exit = code;
 506  0
             this.out = stdout;
 507  0
             this.err = stderr;
 508  0
         }
 509  
 
 510  
         /**
 511  
          * Get {@code code} from the process.
 512  
          * @return Full {@code code} of the process
 513  
          */
 514  
         public int code() {
 515  0
             return this.exit;
 516  
         }
 517  
 
 518  
         /**
 519  
          * Get {@code stdout} from the process.
 520  
          * @return Full {@code stdout} of the process
 521  
          */
 522  
         public String stdout() {
 523  0
             return this.out;
 524  
         }
 525  
 
 526  
         /**
 527  
          * Get {@code stderr} from the process.
 528  
          * @return Full {@code stderr} of the process
 529  
          */
 530  
         public String stderr() {
 531  0
             return this.err;
 532  
         }
 533  
     }
 534  
 }