/*=============================================================================#
# Copyright (c) 2013-2016 Stephan Wahlbrink (WalWare.de) and others.
# All rights reserved. This program and the accompanying materials
# are made available under the terms of the GNU Lesser General Public License
# v2.1 which accompanies this distribution, and is available at
# http://www.gnu.org/licenses/lgpl.html
#
# Contributors:
# Stephan Wahlbrink - initial API and implementation
#=============================================================================*/
package de.walware.rj.server.jri;
import static de.walware.rj.server.jri.JRIServerErrors.LOGGER;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import org.rosuda.JRI.Rengine;
import de.walware.rj.RjInitFailedException;
import de.walware.rj.server.ConsoleWriteCmdItem;
class JRIServerIOStreams {
private static final int CHAR_BUFFER_SIZE= 0x2000;
static interface StreamHandler {
byte getStreamId();
void domexFlush();
}
abstract class AbstractOutPipe extends Thread implements StreamHandler {
private final byte id;
protected final ByteBuffer bb;
protected final ByteBuffer bbNoInput = ByteBuffer.allocate(0);
private final CharsetDecoder decoder;
private boolean working;
public AbstractOutPipe(final byte id, final Charset charset) {
super("OutPipe-" + id); //$NON-NLS-1$
setDaemon(true);
this.id= id;
this.bb= ByteBuffer.allocateDirect(CHAR_BUFFER_SIZE * 2);
this.decoder= charset.newDecoder();
this.decoder.onMalformedInput(CodingErrorAction.REPLACE);
this.decoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
}
@Override
public final byte getStreamId() {
return this.id;
}
protected ConsoleHandler[] getConsoleHandlers() {
final List<ConsoleHandler> consoleHandlers= new ArrayList<>();
final LogManager logManager= LogManager.getLogManager();
for (final Enumeration<String> loggerNames= logManager.getLoggerNames(); loggerNames.hasMoreElements(); ) {
final Logger logger= logManager.getLogger(loggerNames.nextElement());
if (logger != null) {
for (final Handler handler : logger.getHandlers()) {
if (handler instanceof ConsoleHandler
&& !consoleHandlers.contains(handler) ) {
handler.flush();
consoleHandlers.add((ConsoleHandler) handler);
}
}
}
}
return consoleHandlers.toArray(new ConsoleHandler[consoleHandlers.size()]);
}
@Override
public final void run() {
while (true) {
int read;
try {
read= doRead();
}
catch (final IOException e) {
read= -1;
LOGGER.log(Level.SEVERE, "An error occurred when reading output stream.", e);
}
if (read > 0) {
this.bb.position(this.bb.position() + read);
}
this.bb.flip();
JRIServerIOStreams.this.server.mainExchangeLock.lock();
try {
domexBeginStream(this);
DECODE: while (true) {
this.working= true;
CoderResult result= null;
if (read > 0) {
result= this.decoder.decode(this.bb,
JRIServerIOStreams.this.outputBuffer, false );
}
else if (read < 0) {
result= this.decoder.decode(this.bb,
JRIServerIOStreams.this.outputBuffer, true );
}
if (result == CoderResult.OVERFLOW) {
domexSendOut();
continue DECODE;
}
else if (read < 0) {
this.bb.clear();
return;
}
else { // result == CoderResult.UNDERFLOW
break DECODE;
}
}
}
finally {
JRIServerIOStreams.this.server.mainExchangeLock.unlock();
}
this.bb.compact();
}
}
@Override
public final void domexFlush() {
try {
do {
this.working= false;
try {
JRIServerIOStreams.this.mexCondition.awaitNanos(50000000L);
}
catch (final InterruptedException e) {
}
} while (this.working);
CoderResult result= this.decoder.decode(this.bbNoInput,
JRIServerIOStreams.this.outputBuffer, true );
while (result == CoderResult.OVERFLOW) {
domexSendOut();
result= this.decoder.flush(JRIServerIOStreams.this.outputBuffer);
}
}
finally {
this.decoder.reset();
}
}
protected abstract int doRead() throws IOException;
}
private class SysOutPipe extends AbstractOutPipe {
public SysOutPipe(final Charset charset) throws Exception {
super(ConsoleWriteCmdItem.SYS_OUTPUT, charset);
final ConsoleHandler[] consoleHandlers= getConsoleHandlers();
System.out.flush();
System.err.flush();
final int code= Rengine.rniInitSysPipes(this.bb, consoleHandlers);
if (code != 0) {
throw new RjInitFailedException("Error code of InitSysPipes: " + code); //$NON-NLS-1$
}
}
@Override
protected int doRead() throws IOException {
return Rengine.rniReadSysOut(this.bb.position());
}
}
private final JRIServer server;
//-- mex --
private final Condition mexCondition;
private byte currentStreamId;
private final CharBuffer outputBuffer= CharBuffer.allocate(CHAR_BUFFER_SIZE);
private StreamHandler currentHandler;
public JRIServerIOStreams(final JRIServer server) {
this.server= server;
this.mexCondition= this.server.mainExchangeLock.newCondition();
}
void init() {
// Sys pipes
if (!Boolean.parseBoolean(System.getProperty("de.walware.rj.sysout.disable"))) { //$NON-NLS-1$
try {
Charset charset= null;
{ final String enc= System.getProperty("de.walware.rj.sysout.encoding"); //$NON-NLS-1$
if (enc != null) {
try {
charset= Charset.forName(enc);
}
catch (final Exception e) {
LOGGER.log(Level.WARNING, "Failed to setup specified encoding for system output.", e);
}
}
}
{ final String enc= Rengine.rniGetSysOutEnc();
if (enc != null) {
try {
charset= Charset.forName(enc);
}
catch (final Exception e) {}
}
}
if (charset == null) {
charset= Charset.defaultCharset();
}
final AbstractOutPipe abstractOutPipe= new SysOutPipe(charset);
abstractOutPipe.start();
}
catch (final Exception e) {
LOGGER.log(Level.WARNING, "Failed to setup redirect of system pipes.", e);
}
}
}
void domexAppendOut(final byte streamId, final String text) {
if (this.currentStreamId != streamId) {
domexFlushOut();
this.currentStreamId= streamId;
}
else {
if (this.outputBuffer.position() + text.length() > CHAR_BUFFER_SIZE) {
domexSendOut();
}
}
if (text.length() >= CHAR_BUFFER_SIZE) {
this.server.domexAppend2C(new ConsoleWriteCmdItem(streamId, text));
return;
}
{ final int pos= this.outputBuffer.position();
text.getChars(0, text.length(), this.outputBuffer.array(), pos);
this.outputBuffer.position(pos + text.length());
}
}
void domexBeginStream(final StreamHandler handler) {
if (this.currentStreamId != handler.getStreamId()) {
domexFlushOut();
this.currentStreamId= handler.getStreamId();
this.currentHandler= handler;
}
}
boolean domexHasOut() {
return (this.outputBuffer.position() > 0);
}
void domexSendOut() {
if (this.outputBuffer.position() > 0) {
this.outputBuffer.flip();
this.server.domexAppend2C(new ConsoleWriteCmdItem(this.currentStreamId,
this.outputBuffer.toString() ));
this.outputBuffer.clear();
}
}
void domexFlushOut() {
if (this.currentHandler != null) {
try {
this.currentHandler.domexFlush();
}
catch (final Exception e) {
LOGGER.log(Level.SEVERE, "An error occurred when flushing output stream.", e);
}
finally {
this.currentHandler= null;
}
}
if (this.outputBuffer.position() > 0) {
domexSendOut();
}
}
}