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

View File

@ -10,42 +10,12 @@ public abstract class Terminal {
public static final int DEFAULT_HEIGHT = 24; public static final int DEFAULT_HEIGHT = 24;
private volatile boolean echoEnabled; private volatile boolean echoEnabled;
private volatile Thread shutdown;
public Terminal() { 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 init() throws IOException;
public abstract void restore() throws IOException; public abstract void restore() throws IOException;
public void setEchoEnabled(boolean enabled) { 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. * @return a character from whatever underlying input method the terminal has available.
*/ */
public abstract int read(); 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 class TerminalType {
public static final String TYPE = "input.terminal"; 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 AUTO = "auto";
public static final String UNIX = "unix"; public static final String UNIX = "unix";

View File

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

View File

@ -1,29 +1,20 @@
package dorkbox.util.input.unsupported; package dorkbox.util.input.unsupported;
import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import dorkbox.util.bytes.ByteBuffer2; import dorkbox.util.bytes.ByteBuffer2;
import dorkbox.util.input.Terminal; import dorkbox.util.input.Terminal;
import dorkbox.util.input.posix.InputStreamReader;
public class UnsupportedTerminal extends Terminal { public class UnsupportedTerminal extends Terminal {
private final ByteBuffer2 buffer = new ByteBuffer2(8, -1); private ByteBuffer2 buffer = new ByteBuffer2(8, -1);
private BufferedReader reader; private int readerCount = -1;
private String readLine = null; private InputStream in;
private char[] line;
private ThreadLocal<Integer> indexOfStringForReadChar = new ThreadLocal<Integer>() { public UnsupportedTerminal() {
@Override this.in = System.in;
protected Integer initialValue() {
return -1;
}
};
public UnsupportedTerminal(String encoding) {
this.reader = new BufferedReader(new InputStreamReader(System.in, encoding));
} }
@Override @Override
@ -46,46 +37,39 @@ public class UnsupportedTerminal extends Terminal {
@Override @Override
public final int read() { public final int read() {
// if we are reading data (because we are in IDE mode), we want to return ALL // if we are reading data (because we are in IDE mode), we want to return ALL the chars of the line!
// 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) {
// 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. // we have to wait for more data.
try { try {
this.readLine = this.reader.readLine(); InputStream sysIn = this.in;
} catch (IOException e) { int read;
return -1; char asChar;
} this.buffer.clearSecure();
this.line = this.readLine.toCharArray(); while ((read = sysIn.read()) != -1) {
this.buffer.clear(); asChar = (char)read;
for (char c : this.line) { if (asChar == '\n') {
this.buffer.writeChar(c); 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! // EACH thread will have it's own count!
if (readerCount == this.buffer.position()) { if (this.readerCount == this.buffer.position()) {
this.indexOfStringForReadChar.set(-1); this.readerCount = -1;
return '\n'; return '\n';
} else { } 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() { 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 * 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 // when reading from the stdin and outputting to the process
public ProcessProxy(String processName, InputStream inputStreamFromConsole, OutputStream outputStreamToProcess) { public ProcessProxy(String processName, InputStream inputStreamFromConsole, OutputStream outputStreamToProcess) {
is = inputStreamFromConsole; this.is = inputStreamFromConsole;
os = outputStreamToProcess; this.os = outputStreamToProcess;
setName(processName); setName(processName);
setDaemon(true); setDaemon(true);
@ -20,7 +20,7 @@ public class ProcessProxy extends Thread {
public void close() { public void close() {
try { try {
is.close(); this.is.close();
} catch (IOException e) { } 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) // the stream will be closed when the process closes it (usually on exit)
int readInt; int readInt;
if (os == null) { if (this.os == null) {
// just read so it won't block. // just read so it won't block.
while ((readInt = is.read()) != -1) { while ((readInt = this.is.read()) != -1) {
} }
} else { } else {
while ((readInt = is.read()) != -1) { while ((readInt = this.is.read()) != -1) {
os.write(readInt); System.err.println("READ : " + (char)readInt);
this.os.write(readInt);
// flush the output on new line. Works for windows/linux, since \n is always the last char in the sequence. // always flush
if (readInt == '\n') { this.os.flush();
os.flush();
}
} }
} }
} catch (IOException ignore) { } catch (IOException ignore) {
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
} finally { } finally {
try { try {
if (os != null) { if (this.os != null) {
os.flush(); // this goes to the console, so we don't want to close it! this.os.flush(); // this goes to the console, so we don't want to close it!
} }
is.close(); this.is.close();
} catch (IOException ignore) { } catch (IOException ignore) {
} }
} }