Fixed issues with buffering input. Now fixed for windows/posix/unsupported

This commit is contained in:
nathan 2014-10-19 18:27:31 +02:00
parent 3756df22ab
commit 25855ae04d
7 changed files with 134 additions and 152 deletions

View File

@ -109,52 +109,70 @@ public class InputConsole {
private final Object inputLockSingle = new Object();
private final Object inputLockLine = new Object();
private final ObjectPool<ByteBuffer2> pool = ObjectPoolFactory.create(new ByteBuffer2Poolable());
private ThreadLocal<ObjectPoolHolder<ByteBuffer2>> threadBuffer = new ThreadLocal<ObjectPoolHolder<ByteBuffer2>>();
private List<ObjectPoolHolder<ByteBuffer2>> threadBuffersForRead = new CopyOnWriteArrayList<ObjectPoolHolder<ByteBuffer2>>();
private final ObjectPool<ByteBuffer2> pool;
private ThreadLocal<ObjectPoolHolder<ByteBuffer2>> readBuff = new ThreadLocal<ObjectPoolHolder<ByteBuffer2>>();
private List<ObjectPoolHolder<ByteBuffer2>> readBuffers = new CopyOnWriteArrayList<ObjectPoolHolder<ByteBuffer2>>();
private ThreadLocal<Integer> threadBufferCounter = new ThreadLocal<Integer>();
private ThreadLocal<ObjectPoolHolder<ByteBuffer2>> readLineBuff = new ThreadLocal<ObjectPoolHolder<ByteBuffer2>>();
private List<ObjectPoolHolder<ByteBuffer2>> readLineBuffers = new CopyOnWriteArrayList<ObjectPoolHolder<ByteBuffer2>>();
private volatile int readChar = -1;
private final Terminal terminal;
private InputConsole() {
Logger logger = InputConsole.logger;
String readers = System.getProperty(TerminalType.READERS);
int readers2 = 32;
if (readers != null) {
try {
readers2 = Integer.parseInt(readers);
} catch (Exception e) {
}
}
this.pool = ObjectPoolFactory.create(new ByteBuffer2Poolable(), readers2);
String type = System.getProperty(TerminalType.TYPE, TerminalType.AUTO).toLowerCase();
if ("dumb".equals(System.getenv("TERM"))) {
type = TerminalType.NONE;
logger.debug("$TERM=dumb; setting type={}", type);
if (logger.isTraceEnabled()) {
logger.trace("System environment 'TERM'=dumb, creating type=" + type);
}
} else {
if (logger.isTraceEnabled()) {
logger.trace("Creating terminal, type=" + type);
}
}
logger.debug("Creating terminal; type={}", type);
String encoding = Encoding.get();
Terminal t;
try {
if (type.equals(TerminalType.UNIX)) {
t = new UnixTerminal(encoding);
t = new UnixTerminal();
}
else if (type.equals(TerminalType.WIN) || type.equals(TerminalType.WINDOWS)) {
t = new WindowsTerminal();
}
else if (type.equals(TerminalType.NONE) || type.equals(TerminalType.OFF) || type.equals(TerminalType.FALSE)) {
t = new UnsupportedTerminal(encoding);
t = new UnsupportedTerminal();
} else {
if (isIDEAutoDetect()) {
logger.debug("Terminal is in UNSUPPORTED (best guess). Unable to support single key input. Only line input available.");
t = new UnsupportedTerminal(encoding);
t = new UnsupportedTerminal();
} else {
if (OS.isWindows()) {
t = new WindowsTerminal();
} else {
t = new UnixTerminal(encoding);
t = new UnixTerminal();
}
}
}
}
catch (Exception e) {
logger.error("Failed to construct terminal, falling back to unsupported");
t = new UnsupportedTerminal(encoding);
t = new UnsupportedTerminal();
}
try {
@ -162,7 +180,7 @@ public class InputConsole {
}
catch (Throwable e) {
logger.error("Terminal initialization failed, falling back to unsupported");
t = new UnsupportedTerminal(encoding);
t = new UnsupportedTerminal();
try {
t.init();
@ -174,7 +192,7 @@ public class InputConsole {
t.setEchoEnabled(true);
this.terminal = t;
logger.debug("Created Terminal: {} ({}x{})", this.terminal.getClass().getSimpleName(), t.getWidth(), t.getHeight());
logger.debug("Created Terminal: {} ({}x{})", t.getClass().getSimpleName(), t.getWidth(), t.getHeight());
}
// called when the JVM is shutting down.
@ -208,15 +226,41 @@ public class InputConsole {
/** return -1 if no data or bunged-up */
private final int read0() {
synchronized (this.inputLockSingle) {
try {
this.inputLockSingle.wait();
} catch (InterruptedException e) {
return -1;
Integer bufferCounter = this.threadBufferCounter.get();
ObjectPoolHolder<ByteBuffer2> objectPoolHolder = this.readBuff.get();
ByteBuffer2 buffer = null;
if (objectPoolHolder == null) {
bufferCounter = 0;
this.threadBufferCounter.set(bufferCounter);
ObjectPoolHolder<ByteBuffer2> holder = this.pool.take();
buffer = holder.getValue();
this.readBuff.set(holder);
this.readBuffers.add(holder);
} else {
buffer = objectPoolHolder.getValue();
}
if (bufferCounter == buffer.position()) {
synchronized (this.inputLockSingle) {
buffer.setPosition(0);
this.threadBufferCounter.set(0);
try {
this.inputLockSingle.wait();
} catch (InterruptedException e) {
return -1;
}
}
}
return this.readChar;
bufferCounter = this.threadBufferCounter.get();
char c = buffer.readChar(bufferCounter);
bufferCounter += 2;
this.threadBufferCounter.set(bufferCounter);
return c;
}
/** return empty char[] if no data */
@ -237,12 +281,12 @@ public class InputConsole {
// the current line info.
// the threadBufferForRead getting added is the part that is important
if (this.threadBuffer.get() == null) {
if (this.readLineBuff.get() == null) {
ObjectPoolHolder<ByteBuffer2> holder = this.pool.take();
this.threadBuffer.set(holder);
this.threadBuffersForRead.add(holder);
this.readLineBuff.set(holder);
this.readLineBuffers.add(holder);
} else {
this.threadBuffer.get().getValue().clear();
this.readLineBuff.get().getValue().clear();
}
}
@ -254,7 +298,7 @@ public class InputConsole {
}
}
ObjectPoolHolder<ByteBuffer2> objectPoolHolder = this.threadBuffer.get();
ObjectPoolHolder<ByteBuffer2> objectPoolHolder = this.readLineBuff.get();
ByteBuffer2 buffer = objectPoolHolder.getValue();
int len = buffer.position();
if (len == 0) {
@ -267,9 +311,9 @@ public class InputConsole {
// dump the chars in the buffer (safer for passwords, etc)
buffer.clearSecure();
this.threadBuffersForRead.remove(objectPoolHolder);
this.readLineBuffers.remove(objectPoolHolder);
this.pool.release(objectPoolHolder);
this.threadBuffer.set(null);
this.readLineBuff.set(null);
return readChars;
}
@ -302,7 +346,6 @@ public class InputConsole {
while ((typedChar = this.terminal.read()) != -1) {
synchronized (this.inputLock) {
// don't let anyone add a new reader while we are still processing the current actions
asChar = (char) typedChar;
if (logger2.isTraceEnabled()) {
@ -311,22 +354,25 @@ public class InputConsole {
// notify everyone waiting for a character.
synchronized (this.inputLockSingle) {
if (this.terminal.wasSequence() && typedChar == '\n') {
// don't want to forward \n if it was a part of a sequence in the unsupported terminal
// the JIT will short-cut this out if we are not the unsupported terminal
} else {
this.readChar = typedChar;
this.inputLockSingle.notifyAll();
// have to do readChar first (readLine has to deal with \b and \n
for (ObjectPoolHolder<ByteBuffer2> objectPoolHolder : this.readBuffers) {
ByteBuffer2 buffer = objectPoolHolder.getValue();
buffer.writeChar(asChar);
}
this.inputLockSingle.notifyAll();
}
// now to handle readLine stuff
// if we type a backspace key, swallow it + previous in READLINE. READCHAR will have it passed.
if (typedChar == '\b') {
if (asChar == '\b') {
int position = 0;
// clear ourself + one extra.
if (ansiEnabled) {
for (ObjectPoolHolder<ByteBuffer2> objectPoolHolder : this.threadBuffersForRead) {
for (ObjectPoolHolder<ByteBuffer2> objectPoolHolder : this.readLineBuffers) {
ByteBuffer2 buffer = objectPoolHolder.getValue();
// size of the buffer BEFORE our backspace was typed
int length = buffer.position();
@ -363,21 +409,17 @@ public class InputConsole {
out.flush();
}
}
// read-line will ignore backspace
continue;
}
// ignoring \r, because \n is ALWAYS the last character in a new line sequence. (even for windows)
if (asChar == '\n') {
else if (asChar == '\n') {
// ignoring \r, because \n is ALWAYS the last character in a new line sequence. (even for windows)
synchronized (this.inputLockLine) {
this.inputLockLine.notifyAll();
}
} else {
}
else {
// only append if we are not a new line.
// our windows console PREVENTS us from returning '\r' (it truncates '\r\n', and returns just '\n')
for (ObjectPoolHolder<ByteBuffer2> objectPoolHolder : this.threadBuffersForRead) {
for (ObjectPoolHolder<ByteBuffer2> objectPoolHolder : this.readLineBuffers) {
ByteBuffer2 buffer = objectPoolHolder.getValue();
buffer.writeChar(asChar);
}

View File

@ -10,42 +10,12 @@ public abstract class Terminal {
public static final int DEFAULT_HEIGHT = 24;
private volatile boolean echoEnabled;
private volatile Thread shutdown;
public Terminal() {
if (this.shutdown != null) {
try {
Runtime.getRuntime().removeShutdownHook(this.shutdown);
}
catch (IllegalStateException e) {
// The VM is shutting down, ignore
}
}
// Register a task to restore the terminal on shutdown
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
restore();
} catch (IOException e) {
Terminal.this.logger.error("Unable to restore the terminal", e);
}
}
};
this.shutdown = new Thread(runnable, "Terminal");
try {
Runtime.getRuntime().addShutdownHook(this.shutdown);
}
catch (IllegalStateException e) {
// The VM is shutting down, ignore
}
}
public abstract void init() throws IOException;
public abstract void restore() throws IOException;
public void setEchoEnabled(boolean enabled) {
@ -63,11 +33,4 @@ public abstract class Terminal {
* @return a character from whatever underlying input method the terminal has available.
*/
public abstract int read();
/**
* Only needed for unsupported character input.
*/
public boolean wasSequence() {
return false;
}
}

View File

@ -2,6 +2,7 @@ package dorkbox.util.input;
public class TerminalType {
public static final String TYPE = "input.terminal";
public static final String READERS = "input.terminal.readers";
public static final String AUTO = "auto";
public static final String UNIX = "unix";

View File

@ -6,6 +6,7 @@ import java.nio.ByteBuffer;
import com.sun.jna.Native;
import dorkbox.util.input.Encoding;
import dorkbox.util.input.Terminal;
/**
@ -25,7 +26,8 @@ public class UnixTerminal extends Terminal {
private ByteBuffer windowSizeBuffer = ByteBuffer.allocate(8);
public UnixTerminal(String encoding) throws Exception {
public UnixTerminal() throws Exception {
String encoding = Encoding.get();
this.reader = new InputStreamReader(System.in, encoding);
this.term = (PosixTerminalControl) Native.loadLibrary("c", PosixTerminalControl.class);

View File

@ -1,29 +1,20 @@
package dorkbox.util.input.unsupported;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import dorkbox.util.bytes.ByteBuffer2;
import dorkbox.util.input.Terminal;
import dorkbox.util.input.posix.InputStreamReader;
public class UnsupportedTerminal extends Terminal {
private final ByteBuffer2 buffer = new ByteBuffer2(8, -1);
private ByteBuffer2 buffer = new ByteBuffer2(8, -1);
private BufferedReader reader;
private String readLine = null;
private char[] line;
private int readerCount = -1;
private InputStream in;
private ThreadLocal<Integer> indexOfStringForReadChar = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return -1;
}
};
public UnsupportedTerminal(String encoding) {
this.reader = new BufferedReader(new InputStreamReader(System.in, encoding));
public UnsupportedTerminal() {
this.in = System.in;
}
@Override
@ -46,46 +37,39 @@ public class UnsupportedTerminal extends Terminal {
@Override
public final int read() {
// if we are reading data (because we are in IDE mode), we want to return ALL
// the chars of the line!
// so, readChar is REALLY the index at which we return letters (until the whole string is returned)
int readerCount = this.indexOfStringForReadChar.get();
if (readerCount == -1) {
// if we are reading data (because we are in IDE mode), we want to return ALL the chars of the line!
// so, 'readerCount' is REALLY the index at which we return letters (until the whole string is returned)
if (this.readerCount == -1) {
// we have to wait for more data.
try {
this.readLine = this.reader.readLine();
} catch (IOException e) {
return -1;
}
InputStream sysIn = this.in;
int read;
char asChar;
this.buffer.clearSecure();
this.line = this.readLine.toCharArray();
this.buffer.clear();
for (char c : this.line) {
this.buffer.writeChar(c);
while ((read = sysIn.read()) != -1) {
asChar = (char)read;
if (asChar == '\n') {
this.readerCount = this.buffer.position();
this.buffer.rewind();
break;
} else {
this.buffer.writeChar(asChar);
}
}
} catch (IOException e1) {
}
readerCount = 0;
this.indexOfStringForReadChar.set(0);
}
// EACH thread will have it's own count!
if (readerCount == this.buffer.position()) {
this.indexOfStringForReadChar.set(-1);
if (this.readerCount == this.buffer.position()) {
this.readerCount = -1;
return '\n';
} else {
this.indexOfStringForReadChar.set(readerCount+2); // because 2 bytes per char in java
char c = this.buffer.readChar();
return c;
}
char c = this.buffer.readChar(readerCount);
return c;
}
@Override
public final boolean wasSequence() {
return this.line.length > 0;
}
}

View File

@ -7,14 +7,6 @@ public class ObjectPoolFactory {
private ObjectPoolFactory() {
}
/**
* Creates a pool with the max number of available processors as the pool size (padded by 2x as many).
*/
public static <T> ObjectPool<T> create(PoolableObject<T> poolableObject) {
return create(poolableObject, Runtime.getRuntime().availableProcessors() * 2);
}
/**
* Creates a pool of the specified size
*/

View File

@ -11,8 +11,8 @@ public class ProcessProxy extends Thread {
// when reading from the stdin and outputting to the process
public ProcessProxy(String processName, InputStream inputStreamFromConsole, OutputStream outputStreamToProcess) {
is = inputStreamFromConsole;
os = outputStreamToProcess;
this.is = inputStreamFromConsole;
this.os = outputStreamToProcess;
setName(processName);
setDaemon(true);
@ -20,7 +20,7 @@ public class ProcessProxy extends Thread {
public void close() {
try {
is.close();
this.is.close();
} catch (IOException e) {
}
}
@ -32,28 +32,26 @@ public class ProcessProxy extends Thread {
// the stream will be closed when the process closes it (usually on exit)
int readInt;
if (os == null) {
if (this.os == null) {
// just read so it won't block.
while ((readInt = is.read()) != -1) {
while ((readInt = this.is.read()) != -1) {
}
} else {
while ((readInt = is.read()) != -1) {
os.write(readInt);
// flush the output on new line. Works for windows/linux, since \n is always the last char in the sequence.
if (readInt == '\n') {
os.flush();
}
while ((readInt = this.is.read()) != -1) {
System.err.println("READ : " + (char)readInt);
this.os.write(readInt);
// always flush
this.os.flush();
}
}
} catch (IOException ignore) {
} catch (IllegalArgumentException e) {
} finally {
try {
if (os != null) {
os.flush(); // this goes to the console, so we don't want to close it!
if (this.os != null) {
this.os.flush(); // this goes to the console, so we don't want to close it!
}
is.close();
this.is.close();
} catch (IOException ignore) {
}
}