All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.jcabi.log.VerboseProcess Maven / Gradle / Ivy

There is a newer version: 1.2.2.1-jre17
Show newest version
/**
 * Copyright (c) 2012-2017, jcabi.com
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met: 1) Redistributions of source code must retain the above
 * copyright notice, this list of conditions and the following
 * disclaimer. 2) Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following
 * disclaimer in the documentation and/or other materials provided
 * with the distribution. 3) Neither the name of the jcabi.com nor
 * the names of its contributors may be used to endorse or promote
 * products derived from this software without specific prior written
 * permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jcabi.log;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.nio.channels.Channels;
import java.nio.channels.ClosedByInterruptException;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import lombok.EqualsAndHashCode;
import lombok.ToString;

/**
 * Utility class for getting {@code stdout} from a running process
 * and logging it through SLF4J.
 *
 * 

For example: * *

 String name = new VerboseProcess(
 *   new ProcessBuilder("who", "am", "i")
 * ).stdout();
* *

The class throws an exception if the process returns a non-zero exit * code. * *

The class is thread-safe. * * @author Yegor Bugayenko ([email protected]) * @version $Id: ec4f06e8fccf33e15ef0df274092cc0572a01590 $ * @since 0.5 */ @ToString @EqualsAndHashCode(of = "process") @SuppressWarnings({ "PMD.DoNotUseThreads", "PMD.TooManyMethods" }) public final class VerboseProcess implements Closeable { /** * Charset. */ private static final String UTF_8 = "UTF-8"; /** * Number of stream monitors. */ private static final int N_MONITORS = 2; /** * The process we're working with. */ private final transient Process process; /** * Log level for stdout. */ private final transient Level olevel; /** * Log level for stderr. */ private final transient Level elevel; /** * Stream monitors. */ private final transient Thread[] monitors; /** * Flag to indicate the closure of this process. */ private transient boolean closed; /** * Public ctor. * @param prc The process to work with */ public VerboseProcess(final Process prc) { this(prc, Level.INFO, Level.WARNING); } /** * Public ctor (builder will be configured to redirect error input to * the {@code stdout} and will receive an empty {@code stdin}). * @param builder Process builder to work with */ public VerboseProcess(final ProcessBuilder builder) { this(VerboseProcess.start(builder)); } /** * Public ctor, with a given process and logging levels for {@code stdout} * and {@code stderr}. Neither {@code stdout} nor {@code stderr} cannot be * set to {@link Level#ALL} because it is intended to be used only for * internal configuration. * @param prc Process to execute and monitor * @param stdout Log level for stdout * @param stderr Log level for stderr * @since 0.11 */ @SuppressWarnings("PMD.ConstructorOnlyInitializesOrCallOtherConstructors") public VerboseProcess(final Process prc, final Level stdout, final Level stderr) { if (prc == null) { throw new IllegalArgumentException("process can't be NULL"); } if (stdout == null) { throw new IllegalArgumentException("stdout LEVEL can't be NULL"); } if (stderr == null) { throw new IllegalArgumentException("stderr LEVEL can't be NULL"); } if (Level.ALL.equals(stdout)) { throw new IllegalArgumentException( // @checkstyle LineLength (1 line) "stdout LEVEL can't be set to ALL because it is intended only for internal configuration" ); } if (Level.ALL.equals(stderr)) { throw new IllegalArgumentException( // @checkstyle LineLength (1 line) "stderr LEVEL can't be set to ALL because it is intended only for internal configuration" ); } this.process = prc; this.olevel = stdout; this.elevel = stderr; this.monitors = new Thread[VerboseProcess.N_MONITORS]; } /** * Public ctor, with a given process and logging levels for {@code stdout} * and {@code stderr}. * @param bdr Process builder to execute and monitor * @param stdout Log level for stdout * @param stderr Log level for stderr * @since 0.12 */ public VerboseProcess(final ProcessBuilder bdr, final Level stdout, final Level stderr) { this(VerboseProcess.start(bdr), stdout, stderr); } /** * Get {@code stdout} from the process, after its finish (the method will * wait for the process and log its output). * *

The method will check process exit code, and if it won't be equal * to zero a runtime exception will be thrown. A non-zero exit code * usually is an indicator of problem. If you want to ignore this code, * use {@link #stdoutQuietly()} instead. * * @return Full {@code stdout} of the process */ public String stdout() { return this.stdout(true); } /** * Get {@code stdout} from the process, after its finish (the method will * wait for the process and log its output). * *

This method ignores exit code of the process. Even if it is * not equal to zero (which usually is an indicator of an error), the * method will quietly return its output. The method is useful when * you're running a background process. You will kill it with * {@link Process#destroy()}, which usually will lead to a non-zero * exit code, which you want to ignore. * * @return Full {@code stdout} of the process * @since 0.10 */ public String stdoutQuietly() { return this.stdout(false); } /** * Wait for the process to stop, logging its output in parallel. * @return Stdout produced by the process * @throws InterruptedException If interrupted in between */ public VerboseProcess.Result waitFor() throws InterruptedException { final CountDownLatch done = new CountDownLatch( VerboseProcess.N_MONITORS ); final ByteArrayOutputStream stdout = new ByteArrayOutputStream(); final ByteArrayOutputStream stderr = new ByteArrayOutputStream(); this.launchMonitors(done, stdout, stderr); int code = 0; try { code = this.process.waitFor(); } finally { Logger.debug( this, "#waitFor(): process finished: %s", this.process ); if (!done.await(2L, TimeUnit.SECONDS)) { Logger.error(this, "#wait() failed"); } } try { return new VerboseProcess.Result( code, stdout.toString(VerboseProcess.UTF_8), stderr.toString(VerboseProcess.UTF_8) ); } catch (final UnsupportedEncodingException ex) { throw new IllegalStateException(ex); } } @Override public void close() { synchronized (this.monitors) { this.closed = true; } for (final Thread monitor : this.monitors) { if (monitor != null) { monitor.interrupt(); Logger.debug(this, "monitor interrupted"); } } this.process.destroy(); Logger.debug(this, "underlying process destroyed"); } /** * Start a process from the given builder. * @param builder Process builder to work with * @return Process started */ private static Process start(final ProcessBuilder builder) { if (builder == null) { throw new IllegalArgumentException("builder can't be NULL"); } try { final Process process = builder.start(); process.getOutputStream().close(); return process; } catch (final IOException ex) { throw new IllegalStateException(ex); } } /** * Get standard output and check for non-zero exit code (if required). * @param check TRUE if we should check for non-zero exit code * @return Full {@code stdout} of the process */ @SuppressWarnings("PMD.PrematureDeclaration") private String stdout(final boolean check) { final long start = System.currentTimeMillis(); final VerboseProcess.Result result; try { result = this.waitFor(); } catch (final InterruptedException ex) { Thread.currentThread().interrupt(); throw new IllegalStateException(ex); } Logger.debug( this, "#stdout(): process %s completed (code=%d, size=%d) in %[ms]s", this.process, result.code(), result.stdout().length(), System.currentTimeMillis() - start ); if (check && result.code() != 0) { throw new IllegalArgumentException( Logger.format( "Non-zero exit code %d: %[text]s", result.code(), result.stdout() ) ); } return result.stdout(); } /** * Launch monitors for the underlying process. * @param done Latch that signals termination of all monitors * @param stdout Stream to write the underlying process's output * @param stderr Stream to wrint the underlying process's error output */ private void launchMonitors( final CountDownLatch done, final ByteArrayOutputStream stdout, final ByteArrayOutputStream stderr) { synchronized (this.monitors) { if (this.closed) { done.countDown(); done.countDown(); } else { this.monitors[0] = this.monitor( this.process.getInputStream(), done, stdout, this.olevel, "out" ); Logger.debug( this, "#waitFor(): waiting for stdout of %s in %s...", this.process, this.monitors[0] ); this.monitors[1] = this.monitor( this.process.getErrorStream(), done, stderr, this.elevel, "err" ); Logger.debug( this, "#waitFor(): waiting for stderr of %s in %s...", this.process, this.monitors[1] ); } } } /** * Monitor this input input. * @param input Stream to monitor * @param done Count down latch to signal when done * @param output Buffer to write to * @param level Logging level * @param name Name of this monitor. Used in logging as part of threadname * @return Thread which is monitoring * @checkstyle ParameterNumber (6 lines) */ private Thread monitor(final InputStream input, final CountDownLatch done, final OutputStream output, final Level level, final String name) { final Thread thread = new Thread( new VerboseRunnable( new VerboseProcess.Monitor(input, done, output, level), false ) ); thread.setName( String.format( "VrbPrc.Monitor-%d-%s", this.hashCode(), name ) ); thread.setDaemon(true); thread.start(); return thread; } /** * Close quietly. * @param res Resource to close */ private static void close(final Closeable res) { try { res.close(); } catch (final IOException ex) { Logger.error( VerboseProcess.class, "failed to close resource: %[exception]s", ex ); } } /** * Stream monitor. */ private static final class Monitor implements Callable { /** * Stream to read. */ private final transient InputStream input; /** * Latch to count down when done. */ private final transient CountDownLatch done; /** * Buffer to save output. */ private final transient OutputStream output; /** * Log level. */ private final transient Level level; /** * Ctor. * @param inp Stream to monitor * @param latch Count down latch to signal when done * @param out Buffer to write to * @param lvl Logging level * @checkstyle ParameterNumber (5 lines) */ Monitor(final InputStream inp, final CountDownLatch latch, final OutputStream out, final Level lvl) { this.input = inp; this.done = latch; this.output = out; this.level = lvl; } @Override public Void call() throws Exception { final BufferedReader reader = new BufferedReader( Channels.newReader( Channels.newChannel(this.input), VerboseProcess.UTF_8 ) ); try { final BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(this.output, VerboseProcess.UTF_8) ); try { while (true) { if (Thread.interrupted()) { Logger.debug( VerboseProcess.class, "explicitly interrupting read from buffer" ); break; } final String line = reader.readLine(); if (line == null) { break; } Logger.log( this.level, VerboseProcess.class, ">> %s", line ); writer.write(line); writer.newLine(); } } catch (final ClosedByInterruptException ex) { Thread.interrupted(); Logger.debug( VerboseProcess.class, "Monitor is interrupted in the expected way" ); } catch (final IOException ex) { Logger.error( VerboseProcess.class, "Error reading from process stream: %[exception]s", ex ); } finally { VerboseProcess.close(writer); this.done.countDown(); } } finally { VerboseProcess.close(reader); } return null; } } /** * Class representing the result of a process. */ public static final class Result { /** * Returned code from the process. */ private final transient int exit; /** * {@code stdout} from the process. */ private final transient String out; /** * {@code stderr} from the process. */ private final transient String err; /** * Result class constructor. * @param code The exit code. * @param stdout The {@code stdout} from the process. * @param stderr The {@code stderr} from the process. */ Result(final int code, final String stdout, final String stderr) { this.exit = code; this.out = stdout; this.err = stderr; } /** * Get {@code code} from the process. * @return Full {@code code} of the process */ public int code() { return this.exit; } /** * Get {@code stdout} from the process. * @return Full {@code stdout} of the process */ public String stdout() { return this.out; } /** * Get {@code stderr} from the process. * @return Full {@code stderr} of the process */ public String stderr() { return this.err; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy