001package co.codewizards.cloudstore.core.util.childprocess;
002
003import static java.util.Objects.*;
004
005import java.io.IOException;
006import java.io.InputStream;
007import java.io.OutputStream;
008
009import org.slf4j.Logger;
010import org.slf4j.LoggerFactory;
011
012/**
013 * Thread continuously reading from an {@link InputStream} and writing everything to an {@link OutputStream}.
014 * <p>
015 * While this functionality is useful for multiple purposes, we use {@code DumpStreamThread} primarily for
016 * child processes. When starting another process (e.g. via the
017 * {@link java.lang.ProcessBuilder ProcessBuilder}), it is necessary to read the process' output. Without
018 * doing this, the child process might block forever - especially on Windows (having a very limited
019 * standard-output-buffer), this is a well-known problem.
020 * <p>
021 * Since the main thread (invoking the child process) usually blocks and waits for the child process to
022 * return (i.e. exit), dumping its standard-out and standard-error to a buffer or a log file is done on a
023 * separate thread.
024 * <p>
025 * Please note that {@code DumpStreamThread} can automatically write everything to the log. This is done via
026 * an instance of {@link LogDumpedStreamThread}.
027 * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
028 */
029public class DumpStreamThread extends Thread
030{
031        private final Logger logger = LoggerFactory.getLogger(DumpStreamThread.class);
032        private final InputStream inputStream;
033        private final OutputStream outputStream;
034        private volatile boolean ignoreErrors = false;
035        private volatile boolean forceInterrupt = false;
036        private final LogDumpedStreamThread logDumpedStreamThread;
037
038        public void setIgnoreErrors(final boolean ignoreErrors) {
039                this.ignoreErrors = ignoreErrors;
040        }
041
042        @Override
043        public void interrupt() {
044                forceInterrupt = true;
045                super.interrupt();
046        }
047
048        @Override
049        public boolean isInterrupted() {
050                return forceInterrupt || super.isInterrupted();
051        }
052
053        /**
054         * Creates an instance of {@code DumpStreamThread}.
055         * @param inputStream the stream to read from. Must not be <code>null</code>.
056         * @param outputStream the stream to write to. Must not be <code>null</code>.
057         * @param childProcessLoggerName the name of the logger. May be <code>null</code> if logging is not
058         * desired. In case of <code>null</code>, logging is completely disabled.
059         */
060        public DumpStreamThread(final InputStream inputStream, final OutputStream outputStream, final String childProcessLoggerName) {
061                this(inputStream, outputStream, null, childProcessLoggerName);
062        }
063
064        /**
065         * Creates an instance of {@code DumpStreamThread}.
066         * @param inputStream the stream to read from. Must not be <code>null</code>.
067         * @param outputStream the stream to write to. Must not be <code>null</code>.
068         * @param childProcessLogger the logger. May be <code>null</code> if logging is not desired.
069         */
070        public DumpStreamThread(final InputStream inputStream, final OutputStream outputStream, final Logger childProcessLogger) {
071                this(inputStream, outputStream, childProcessLogger, null);
072        }
073
074        private DumpStreamThread(final InputStream inputStream, final OutputStream outputStream,
075                        final Logger childProcessLogger, final String childProcessLoggerName)
076        {
077                requireNonNull(inputStream, "inputStream");
078                requireNonNull(outputStream, "outputStream");
079
080                this.inputStream = inputStream;
081                this.outputStream = outputStream;
082                if (childProcessLogger != null)
083                        this.logDumpedStreamThread = new LogDumpedStreamThread(childProcessLogger);
084                else
085                        this.logDumpedStreamThread = childProcessLoggerName == null ? null : new LogDumpedStreamThread(childProcessLoggerName);
086        }
087
088        @Override
089        public synchronized void start() {
090                if (logDumpedStreamThread != null)
091                        logDumpedStreamThread.start();
092
093                super.start();
094        }
095
096        @Override
097        public void run() {
098                try {
099                        final byte[] buffer = new byte[10240];
100                        while (!isInterrupted()) {
101                                try {
102                                        final int bytesRead = inputStream.read(buffer);
103                                        if (bytesRead > 0) {
104                                                outputStream.write(buffer, 0, bytesRead);
105
106                                                if (logDumpedStreamThread != null)
107                                                        logDumpedStreamThread.write(buffer, bytesRead);
108                                        }
109                                } catch (final Throwable e) {
110                                        if (!ignoreErrors)
111                                                logger.error("run: " + e, e); //$NON-NLS-1$
112                                        else
113                                                logger.info("run: " + e); //$NON-NLS-1$
114
115                                        return;
116                                }
117                        }
118                } finally {
119                        if (logDumpedStreamThread != null)
120                                logDumpedStreamThread.interrupt();
121
122                        try {
123                                outputStream.close();
124                        } catch (final IOException e) {
125                                logger.warn("run: outputStream.close() failed: " + e, e); //$NON-NLS-1$
126                        }
127                }
128        }
129
130        /**
131         * Sets a {@link StringBuffer} for capturing all output.
132         * <p>
133         * Please note, that only data read from the stream after this was set is captured. You normally want to
134         * set this {@code StringBuffer}.
135         * <p>
136         * This feature is only available, if logging is enabled.
137         * @param outputStringBuffer the {@link StringBuffer} used for capturing. May be <code>null</code>.
138         */
139        public void setOutputStringBuffer(final StringBuffer outputStringBuffer) {
140                if (logDumpedStreamThread == null)
141                        throw new IllegalStateException("Not supported, if logging is disabled!");
142
143                logDumpedStreamThread.setOutputStringBuffer(outputStringBuffer);
144        }
145        public StringBuffer getOutputStringBuffer() {
146                if (logDumpedStreamThread == null)
147                        throw new IllegalStateException("Not supported, if logging is disabled!");
148
149                return logDumpedStreamThread.getOutputStringBuffer();
150        }
151        public void setOutputStringBufferMaxLength(final int outputStringBufferMaxLength) {
152                if (logDumpedStreamThread == null)
153                        throw new IllegalStateException("Not supported, if logging is disabled!");
154
155                logDumpedStreamThread.setOutputStringBufferMaxLength(outputStringBufferMaxLength);
156        }
157        public int getOutputStringBufferMaxLength() {
158                if (logDumpedStreamThread == null)
159                        throw new IllegalStateException("Not supported, if logging is disabled!");
160
161                return logDumpedStreamThread.getOutputStringBufferMaxLength();
162        }
163
164        public void flushBuffer() {
165                if (logDumpedStreamThread != null)
166                        logDumpedStreamThread.flushBuffer();
167        }
168}