package co.codewizards.cloudstore.core.util.childprocess;
import java.io.BufferedReader;
import co.codewizards.cloudstore.core.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Thread used to log standard-out or standard-error from a child-process.
* <p>
* Besides logging, it can write all data to a {@link StringBuffer}.
* <p>
* An instance of this class is usally not created explicitly, but implicitly via an instance of
* {@link DumpStreamThread}.
* @author Marco หงุ่ยตระกูล-Schulze - marco at codewizards dot co
*/
public class LogDumpedStreamThread extends Thread
{
/**
* UTF-8 character set name.
*/
private static final String CHARSET_NAME_UTF_8 = "UTF-8";
private static final Logger logger = LoggerFactory.getLogger(LogDumpedStreamThread.class);
/**
* If the first data which arrived here via {@link #write(byte[], int)} have not yet been written
* after this time (i.e. their age exceeds this time) in milliseconds, it is logged.
*/
private static final long logMaxAge = 5000L;
/**
* If there was no write after this many milliseconds, the current buffer is logged.
*/
private static final long logAfterNoWritePeriod = 500L;
/**
* If the buffer grows bigger than this size in bytes, it is logged - no matter when the last
* write occured.
*/
private static final int logWhenBufferExceedsSize = 50 * 1024; // 50 KB
private final ByteArrayOutputStream bufferOutputStream = new ByteArrayOutputStream();
private volatile boolean forceInterrupt = false;
private Long firstNonLoggedWriteTimestamp = null;
private long lastWriteTimestamp = 0;
private volatile StringBuffer outputStringBuffer;
private volatile int outputStringBufferMaxLength = 1024 * 1024;
private Logger childProcessLogger;
public LogDumpedStreamThread(final String childProcessLoggerName) {
this(childProcessLoggerName == null ? null : LoggerFactory.getLogger(childProcessLoggerName));
}
public LogDumpedStreamThread(final Logger childProcessLogger)
{
if (childProcessLogger == null)
this.childProcessLogger = logger;
else
this.childProcessLogger = childProcessLogger;
}
public void write(final byte[] data, final int length)
{
if (data == null)
throw new IllegalArgumentException("data == null"); //$NON-NLS-1$
synchronized (bufferOutputStream) {
bufferOutputStream.write(data, 0, length);
lastWriteTimestamp = System.currentTimeMillis();
if (firstNonLoggedWriteTimestamp == null)
firstNonLoggedWriteTimestamp = lastWriteTimestamp;
}
}
public void setOutputStringBuffer(final StringBuffer outputStringBuffer) {
this.outputStringBuffer = outputStringBuffer;
}
public StringBuffer getOutputStringBuffer() {
return outputStringBuffer;
}
public void setOutputStringBufferMaxLength(final int outputStringBufferMaxLength) {
this.outputStringBufferMaxLength = outputStringBufferMaxLength;
}
public int getOutputStringBufferMaxLength() {
return outputStringBufferMaxLength;
}
@Override
public void interrupt() {
forceInterrupt = true;
super.interrupt();
}
@Override
public boolean isInterrupted() {
return forceInterrupt || super.isInterrupted();
}
@Override
public void run() {
while (!isInterrupted()) {
try {
synchronized (bufferOutputStream) {
try {
bufferOutputStream.wait(500L);
} catch (final InterruptedException x) {
// doNothing(); TODO reference to the Util.doNothing class after Util moved into this util project!
}
processBuffer(false);
}
} catch (final Throwable e) {
logger.error("run: " + e, e); //$NON-NLS-1$
}
}
processBuffer(true);
}
public void flushBuffer()
{
processBuffer(true);
}
protected void processBuffer(final boolean force)
{
synchronized (bufferOutputStream) {
if (bufferOutputStream.size() > 0) {
final long firstNonLoggedWriteAge = firstNonLoggedWriteTimestamp == null ? 0 : System.currentTimeMillis() - firstNonLoggedWriteTimestamp;
final long noWritePeriod = System.currentTimeMillis() - lastWriteTimestamp;
if (force || firstNonLoggedWriteAge > logMaxAge || noWritePeriod > logAfterNoWritePeriod || bufferOutputStream.size() > logWhenBufferExceedsSize) {
String currentBufferString;
try {
currentBufferString = bufferOutputStream.toString(CHARSET_NAME_UTF_8);
} catch (final UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
final StringBuffer outputStringBuffer = getOutputStringBuffer();
if (outputStringBuffer != null) {
final int newOutputStringBufferLength = outputStringBuffer.length() + currentBufferString.length();
if (newOutputStringBufferLength > outputStringBufferMaxLength) {
int lastCharPositionToDelete = newOutputStringBufferLength - outputStringBufferMaxLength;
// search for first line-break
while (outputStringBuffer.length() > lastCharPositionToDelete && outputStringBuffer.charAt(lastCharPositionToDelete) != '\n')
++lastCharPositionToDelete;
lastCharPositionToDelete = Math.min(lastCharPositionToDelete, outputStringBuffer.length() - 1);
outputStringBuffer.delete(0, lastCharPositionToDelete + 1);
}
outputStringBuffer.append(currentBufferString);
}
childProcessLogger.info(
'\n' + prefixEveryLine(currentBufferString)
);
bufferOutputStream.reset();
}
}
}
}
private String prefixEveryLine(final String s)
{
try {
final StringBuilder result = new StringBuilder();
final String prefix = " >>> "; //$NON-NLS-1$
final BufferedReader r = new BufferedReader(new StringReader(s));
String line;
while (null != (line = r.readLine()))
result.append(prefix).append(line).append('\n');
return result.toString();
} catch (final IOException x) {
throw new RuntimeException(x);
}
}
}