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

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

There is a newer version: 2.1.2
Show newest version
/**
 * Copyright (c) 2012-2014, 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.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
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$ * @since 0.5 */ @ToString @EqualsAndHashCode(of = "process") public final class VerboseProcess { /** * 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; /** * 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}. * @param prc Process to execute and monitor * @param stdout Log level for stdout * @param stderr Log level for stderr * @since 0.11 */ 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"); } this.process = prc; this.olevel = stdout; this.elevel = stderr; } /** * 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); } /** * 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 String stdout; try { stdout = this.waitFor(); } catch (final InterruptedException ex) { Thread.currentThread().interrupt(); throw new IllegalStateException(ex); } final int code = this.process.exitValue(); Logger.debug( this, "#stdout(): process %s completed (code=%d, size=%d) in %[ms]s", this.process, code, stdout.length(), System.currentTimeMillis() - start ); if (check && code != 0) { throw new IllegalArgumentException( Logger.format("Non-zero exit code %d: %[text]s", code, stdout) ); } return stdout; } /** * Wait for the process to stop, logging its output in parallel. * @return Stdout produced by the process * @throws InterruptedException If interrupted in between */ private String waitFor() throws InterruptedException { final CountDownLatch done = new CountDownLatch(2); final ByteArrayOutputStream stdout = new ByteArrayOutputStream(); Logger.debug( this, "#waitFor(): waiting for stdout of %s in %s...", this.process, this.monitor( this.process.getInputStream(), done, stdout, this.olevel ) ); Logger.debug( this, "#waitFor(): waiting for stderr of %s in %s...", this.process, this.monitor( this.process.getErrorStream(), done, new ByteArrayOutputStream(), this.elevel ) ); try { 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 { // @checkstyle MultipleStringLiteralsCheck (1 line) return stdout.toString("UTF-8"); } catch (final UnsupportedEncodingException ex) { throw new IllegalStateException(ex); } } /** * 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 * @return Thread which is monitoring * @checkstyle ParameterNumber (5 lines) */ @SuppressWarnings("PMD.DoNotUseThreads") private Thread monitor(final InputStream input, final CountDownLatch done, final OutputStream output, final Level level) { final Thread thread = new Thread( new VerboseRunnable( new VerboseProcess.Monitor(input, done, output, level), false ) ); thread.setName("VerboseProcess"); thread.setDaemon(true); thread.start(); return thread; } /** * 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 Charset charset = Charset.forName("UTF-8"); final BufferedReader reader = new BufferedReader( new InputStreamReader(this.input, charset) ); final BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(this.output, charset) ); try { while (true) { final String line = reader.readLine(); if (line == null) { break; } Logger.log( this.level, VerboseProcess.class, ">> %s", line ); writer.write(line); writer.newLine(); } } finally { try { reader.close(); writer.close(); } catch (final IOException ex) { Logger.error( this, "failed to close reader: %[exception]s", ex ); } this.done.countDown(); } return null; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy