diff --git a/Dorkbox-Util/src/dorkbox/util/bytes/ByteBuffer2.java b/Dorkbox-Util/src/dorkbox/util/bytes/ByteBuffer2.java index 5f31b8c..1455774 100644 --- a/Dorkbox-Util/src/dorkbox/util/bytes/ByteBuffer2.java +++ b/Dorkbox-Util/src/dorkbox/util/bytes/ByteBuffer2.java @@ -34,23 +34,23 @@ public class ByteBuffer2 { public ByteBuffer2(byte[] bytes) { this.bytes = bytes; clear(); - position = bytes.length; + this.position = bytes.length; } public byte getByte() { - if (position > limit) { + if (this.position > this.limit) { throw new BufferUnderflowException(); } - return bytes[position++]; + return this.bytes[this.position++]; } public byte getByte(int i) { - if (i > limit) { + if (i > this.limit) { throw new BufferUnderflowException(); } - return bytes[i]; + return this.bytes[i]; } public void getBytes(byte[] buffer) { @@ -62,12 +62,12 @@ public class ByteBuffer2 { } public void getBytes(byte[] buffer, int offset, int length) { - if (position + length - offset > limit) { + if (this.position + length - offset > this.limit) { throw new BufferUnderflowException(); } - System.arraycopy(bytes, position, buffer, 0, length-offset); - position += length-offset; + System.arraycopy(this.bytes, this.position, buffer, 0, length-offset); + this.position += length-offset; } /** @@ -76,18 +76,18 @@ public class ByteBuffer2 { * NOT PROTECTED */ private final void _put(byte b) { - bytes[position++] = b; + this.bytes[this.position++] = b; } /** NOT PROTECTED! */ private final void checkBuffer(int threshold) { - if (bytes.length < threshold) { + if (this.bytes.length < threshold) { byte[] t = new byte[threshold]; // grow at back of array - System.arraycopy(bytes, 0, t, 0, bytes.length); - limit = t.length; + System.arraycopy(this.bytes, 0, t, 0, this.bytes.length); + this.limit = t.length; - bytes = t; + this.bytes = t; } } @@ -101,16 +101,16 @@ public class ByteBuffer2 { } public final synchronized ByteBuffer2 putBytes(byte[] src, int offset, int length) { - checkBuffer(position + length - offset); + checkBuffer(this.position + length - offset); - System.arraycopy(src, offset, bytes, position, length); - position += length; + System.arraycopy(src, offset, this.bytes, this.position, length); + this.position += length; return this; } public final synchronized ByteBuffer2 putByte(byte b) { - checkBuffer(position + 1); + checkBuffer(this.position + 1); _put(b); return this; @@ -122,7 +122,7 @@ public class ByteBuffer2 { } public final synchronized void putChar(char c) { - checkBuffer(position + 2); + checkBuffer(this.position + 2); putBytes(BigEndian.Char_.toBytes(c)); } @@ -131,8 +131,19 @@ public class ByteBuffer2 { return BigEndian.Char_.fromBytes(getByte(), getByte()); } + public final char getChar(int i) { + return BigEndian.Char_.fromBytes(getByte(i++), getByte(i)); + } + + public void getChars(int srcStart, int srcLength, char[] dest, int destStart) { + for (int i=srcStart;i bytes.length || position < 0) { + if (position > this.bytes.length || position < 0) { throw new IllegalArgumentException(); } this.position = position; - if (mark > position) { - mark = -1; + if (this.mark > position) { + this.mark = -1; } return this; @@ -212,7 +232,7 @@ public class ByteBuffer2 { * limit. */ public final synchronized int remaining() { - return limit - position; + return this.limit - this.position; } /** @@ -220,7 +240,7 @@ public class ByteBuffer2 { * the limit. */ public final synchronized boolean hasRemaining() { - return position < limit; + return this.position < this.limit; } /** @@ -228,11 +248,11 @@ public class ByteBuffer2 { */ public final synchronized void limit(int limit) { this.limit = limit; - if (position > limit) { - position = limit; + if (this.position > limit) { + this.position = limit; } - if (mark > limit) { - mark = -1; + if (this.mark > limit) { + this.mark = -1; } } @@ -240,14 +260,14 @@ public class ByteBuffer2 { * Returns this buffer's limit. */ public int limit() { - return limit; + return this.limit; } /** * Returns this buffer's capacity. */ public int capacity() { - return bytes.length; + return this.bytes.length; } /** @@ -260,8 +280,8 @@ public class ByteBuffer2 { * can be followed immediately by an invocation of another relative put method. */ public final synchronized void compact() { - mark = -1; - System.arraycopy(bytes, position, bytes, 0, remaining()); + this.mark = -1; + System.arraycopy(this.bytes, this.position, this.bytes, 0, remaining()); position(remaining()); limit(capacity()); @@ -275,9 +295,9 @@ public class ByteBuffer2 { * discarded. */ public final synchronized void flip() { - limit = position; - position = 0; - mark = -1; + this.limit = this.position; + this.position = 0; + this.mark = -1; } /** @@ -285,9 +305,9 @@ public class ByteBuffer2 { * the capacity, and the mark is discarded. */ public final synchronized void clear() { - position = 0; - limit = capacity(); - mark = -1; + this.position = 0; + this.limit = capacity(); + this.mark = -1; } /** @@ -295,15 +315,15 @@ public class ByteBuffer2 { * discarded. */ public final synchronized void rewind() { - position = 0; - mark = -1; + this.position = 0; + this.mark = -1; } /** * Sets this buffer's mark at its position. */ public final synchronized void mark() { - mark = position; + this.mark = this.position; } /** @@ -313,6 +333,6 @@ public class ByteBuffer2 { * value.

*/ public void reset() { - position = mark; + this.position = this.mark; } } \ No newline at end of file diff --git a/Dorkbox-Util/src/dorkbox/util/bytes/ByteBuffer2Fast.java b/Dorkbox-Util/src/dorkbox/util/bytes/ByteBuffer2Fast.java new file mode 100644 index 0000000..0e26bd5 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/bytes/ByteBuffer2Fast.java @@ -0,0 +1,338 @@ +package dorkbox.util.bytes; + +import java.nio.BufferUnderflowException; + +/** + * Cleanroom implementation of a self-growing bytebuffer. NOT SYNCHRONIZED! + */ +public class ByteBuffer2Fast { + + private byte[] bytes; + + private int position = 0; + private int mark = -1; + private int limit = 0; + + public static ByteBuffer2Fast wrap(byte[] buffer) { + return new ByteBuffer2Fast(buffer); + } + + public static ByteBuffer2Fast allocate(int capacity) { + ByteBuffer2Fast byteBuffer2 = new ByteBuffer2Fast(new byte[capacity]); + byteBuffer2.clear(); + return byteBuffer2; + } + + public ByteBuffer2Fast() { + this(0); + } + + public ByteBuffer2Fast(int size) { + this(new byte[size]); + } + + public ByteBuffer2Fast(byte[] bytes) { + this.bytes = bytes; + clear(); + this.position = bytes.length; + } + + public byte getByte() { + if (this.position > this.limit) { + throw new BufferUnderflowException(); + } + + return this.bytes[this.position++]; + } + + public byte getByte(int i) { + if (i > this.limit) { + throw new BufferUnderflowException(); + } + + return this.bytes[i]; + } + + public void getBytes(byte[] buffer) { + getBytes(buffer, 0, buffer.length); + } + + public void getBytes(byte[] buffer, int length) { + getBytes(buffer, 0, length); + } + + public void getBytes(byte[] buffer, int offset, int length) { + if (this.position + length - offset > this.limit) { + throw new BufferUnderflowException(); + } + + System.arraycopy(this.bytes, this.position, buffer, 0, length-offset); + this.position += length-offset; + } + + /** + * MUST call checkBuffer before calling this! + * + * NOT PROTECTED + */ + private final void _put(byte b) { + this.bytes[this.position++] = b; + } + + /** NOT PROTECTED! */ + private final void checkBuffer(int threshold) { + if (this.bytes.length < threshold) { + byte[] t = new byte[threshold]; + // grow at back of array + System.arraycopy(this.bytes, 0, t, 0, this.bytes.length); + this.limit = t.length; + + this.bytes = t; + } + } + + public final void put(ByteBuffer2Fast buffer) { + putBytes(buffer.array(), buffer.position, buffer.limit); + buffer.position = buffer.limit; + } + + public final ByteBuffer2Fast putBytes(byte[] src) { + return putBytes(src, 0, src.length); + } + + public final ByteBuffer2Fast putBytes(byte[] src, int offset, int length) { + checkBuffer(this.position + length - offset); + + System.arraycopy(src, offset, this.bytes, this.position, length); + this.position += length; + + return this; + } + + public final ByteBuffer2Fast putByte(byte b) { + checkBuffer(this.position + 1); + + _put(b); + return this; + } + + public final void putByte(int position, byte b) { + this.position = position; + putByte(b); + } + + public final void putChar(char c) { + checkBuffer(this.position + 2); + + putBytes(BigEndian.Char_.toBytes(c)); + } + + public final char getChar() { + return BigEndian.Char_.fromBytes(getByte(), getByte()); + } + + public final char getChar(int i) { + return BigEndian.Char_.fromBytes(getByte(i++), getByte(i)); + } + + public void getChars(int srcStart, int srcLength, char[] dest, int destStart) { + for (int i=srcStart;i this.bytes.length || position < 0) { + throw new IllegalArgumentException(); + } + + this.position = position; + if (this.mark > position) { + this.mark = -1; + } + + return this; + } + + /** + * Returns the number of elements between the current position and the + * limit. + */ + public final int remaining() { + return this.limit - this.position; + } + + /** + * Tells whether there are any elements between the current position and + * the limit. + */ + public final boolean hasRemaining() { + return this.position < this.limit; + } + + /** + * Sets this buffer's limit. + */ + public final void limit(int limit) { + this.limit = limit; + if (this.position > limit) { + this.position = limit; + } + if (this.mark > limit) { + this.mark = -1; + } + } + + /** + * Returns this buffer's limit. + */ + public int limit() { + return this.limit; + } + + /** + * Returns this buffer's capacity. + */ + public int capacity() { + return this.bytes.length; + } + + /** + * The bytes between the buffer's current position and its limit, if any, are copied to the beginning of the buffer. + * That is, the byte at index p = position() is copied to index zero, the byte at index p + 1 is copied to index one, + * and so forth until the byte at index limit() - 1 is copied to index n = limit() - 1 - p. The buffer's position is + * then set to n+1 and its limit is set to its capacity. The mark, if defined, is discarded. + * + * The buffer's position is set to the number of bytes copied, rather than to zero, so that an invocation of this method + * can be followed immediately by an invocation of another relative put method. + */ + public final void compact() { + this.mark = -1; + System.arraycopy(this.bytes, this.position, this.bytes, 0, remaining()); + + position(remaining()); + limit(capacity()); + } + + /** + * Readies the buffer for reading. + * + * Flips this buffer. The limit is set to the current position and then + * the position is set to zero. If the mark is defined then it is + * discarded. + */ + public final void flip() { + this.limit = this.position; + this.position = 0; + this.mark = -1; + } + + /** + * Clears this buffer. The position is set to zero, the limit is set to + * the capacity, and the mark is discarded. + */ + public final void clear() { + this.position = 0; + this.limit = capacity(); + this.mark = -1; + } + + /** + * Rewinds this buffer. The position is set to zero and the mark is + * discarded. + */ + public final void rewind() { + this.position = 0; + this.mark = -1; + } + + /** + * Sets this buffer's mark at its position. + */ + public final void mark() { + this.mark = this.position; + } + + /** + * Resets this buffer's position to the previously-marked position. + * + *

Invoking this method neither changes nor discards the mark's + * value.

+ */ + public void reset() { + this.position = this.mark; + } +} \ No newline at end of file diff --git a/Dorkbox-Util/src/dorkbox/util/input/InputConsole.java b/Dorkbox-Util/src/dorkbox/util/input/InputConsole.java index e664cdc..c50784c 100644 --- a/Dorkbox-Util/src/dorkbox/util/input/InputConsole.java +++ b/Dorkbox-Util/src/dorkbox/util/input/InputConsole.java @@ -5,18 +5,21 @@ import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.io.PrintStream; import java.io.Reader; import java.net.URL; import java.nio.charset.Charset; import java.security.CodeSource; import java.security.ProtectionDomain; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.CopyOnWriteArrayList; import org.fusesource.jansi.Ansi; import org.fusesource.jansi.AnsiConsole; import org.slf4j.Logger; import dorkbox.util.OS; +import dorkbox.util.Sys; +import dorkbox.util.bytes.ByteBuffer2Fast; import dorkbox.util.input.posix.UnixTerminal; import dorkbox.util.input.unsupported.UnsupportedTerminal; import dorkbox.util.input.windows.WindowsTerminal; @@ -36,20 +39,31 @@ public class InputConsole { { AnsiConsole.systemInstall(); + Thread consoleThread = new Thread(new Runnable() { + @Override + public void run() { + consoleProxyReader.run(); + } + }); + consoleThread.setDaemon(true); + consoleThread.setName("Console Input Reader"); + + consoleThread.start(); + + // has to be NOT DAEMON thread, since it must run before the app closes. + // don't forget we have to shut down the ansi console as well // alternatively, shut everything down when the JVM closes. - Runtime.getRuntime().addShutdownHook(new Thread() { + Thread shutdownThread = new Thread() { @Override public void run() { AnsiConsole.systemUninstall(); - InputConsole.destroy(); - } - }); - } - // called by our shutdown thread - private static final void destroy() { - consoleProxyReader.destroy0(); + consoleProxyReader.shutdown0(); + } + }; + shutdownThread.setName("Console Input Shutdown"); + Runtime.getRuntime().addShutdownHook(shutdownThread); } /** return null if no data */ @@ -94,9 +108,17 @@ public class InputConsole { private final Object inputLockSingle = new Object(); private final Object inputLockLine = new Object(); - private AtomicBoolean isRunning = new AtomicBoolean(false); - private AtomicBoolean isInShutdown = new AtomicBoolean(false); - private volatile char[] readLine = null; + private ThreadLocal threadBufferForRead = new ThreadLocal(); + private CopyOnWriteArrayList threadBuffersForRead = new CopyOnWriteArrayList(); + + ThreadLocal indexOfStringForReadChar = new ThreadLocal() { + @Override + protected Integer initialValue() { + return -1; + } + }; + + private volatile int readChar = -1; private final boolean unsupported; @@ -119,7 +141,7 @@ public class InputConsole { logger.debug("Creating terminal; type={}", type); - Terminal t; + Terminal t; try { if (type.equals(TerminalType.UNIX)) { t = new UnixTerminal(); @@ -182,36 +204,8 @@ public class InputConsole { logger.debug("Created Terminal: {}", this.terminal); } - /** - * make sure the input console reader thread is started. - */ - private void startInputConsole() { - // protected by atomic! - if (!this.isRunning.compareAndSet(false, true) || this.isInShutdown.get()) { - return; - } - - Thread consoleThread = new Thread(new Runnable() { - @Override - public void run() { - consoleProxyReader.run(); - } - }); - consoleThread.setDaemon(true); - consoleThread.setName("Console Input Reader"); - - consoleThread.start(); - } - - private void destroy0() { - // Don't change this, because we don't want to enable reading, etc from this once it's destroyed. - // so we pretend that it's still running - // isRunning.set(false); - - if (this.isInShutdown.compareAndSet(true, true)) { - return; - } - + // called when the JVM is shutting down. + private void shutdown0() { synchronized (this.inputLockSingle) { this.inputLockSingle.notifyAll(); } @@ -224,7 +218,8 @@ public class InputConsole { InputConsole inputConsole = InputConsole.this; inputConsole.terminal.restore(); - inputConsole.reader.close(); + // this will 'hang' our shutdown, and honestly, who cares? We're shutting down anyways. + // inputConsole.reader.close(); // hangs on shutdown } catch (IOException ignored) { ignored.printStackTrace(); } @@ -241,7 +236,11 @@ public class InputConsole { /** return null if no data */ private final char[] readLine0() { - startInputConsole(); + if (this.threadBufferForRead.get() == null) { + ByteBuffer2Fast buffer = ByteBuffer2Fast.allocate(0); + this.threadBufferForRead.set(buffer); + this.threadBuffersForRead.add(buffer); + } synchronized (this.inputLockLine) { try { @@ -250,34 +249,53 @@ public class InputConsole { return emptyLine; } } - return this.readLine; + + ByteBuffer2Fast stringBuffer = this.threadBufferForRead.get(); + int len = stringBuffer.position(); + if (len == 0) { + return emptyLine; + } + + char[] chars = new char[len/2]; // because 2 chars is 1 bytes + stringBuffer.getChars(0, len, chars, 0); + + // dump the chars in the buffer (safer for passwords, etc) + stringBuffer.clear(); + stringBuffer.putBytes(new byte[0]); + + this.threadBufferForRead.set(null); + this.threadBuffersForRead.remove(stringBuffer); // TODO: use object pool! + + return chars; } /** return null if no data */ private final char[] readLinePassword0() { - // don't bother in an IDE. it won't work. - return readLine0(); - } - - ThreadLocal indexOfStringForReadChar = new ThreadLocal() { - @Override - protected Integer initialValue() { - return -1; + if (this.threadBufferForRead.get() == null) { + ByteBuffer2Fast buffer = ByteBuffer2Fast.allocate(0); + this.threadBufferForRead.set(buffer); + this.threadBuffersForRead.add(buffer); } - }; + + // don't bother in an IDE. it won't work. + boolean echoEnabled = this.terminal.isEchoEnabled(); + this.terminal.setEchoEnabled(false); + char[] readLine0 = readLine0(); + this.terminal.setEchoEnabled(echoEnabled); + + return readLine0; + } /** return -1 if no data */ private final int read0() { - startInputConsole(); - // 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 if (this.unsupported) { - int integer = this.indexOfStringForReadChar.get(); + int readerCount = this.indexOfStringForReadChar.get(); - if (integer == -1) { + if (readerCount == -1) { // we have to wait for more data. synchronized (this.inputLockLine) { try { @@ -285,24 +303,23 @@ public class InputConsole { } catch (InterruptedException e) { return -1; } - integer = 0; + readerCount = 0; this.indexOfStringForReadChar.set(0); } } - // EACH thread will have it's own count! - char[] readLine2 = this.readLine; - if (readLine2 == null) { - return -1; - } else if (integer == readLine2.length) { + // EACH thread will have it's own count! + ByteBuffer2Fast stringBuffer = this.threadBufferForRead.get(); + + if (readerCount == stringBuffer.position()) { this.indexOfStringForReadChar.set(-1); return '\n'; } else { - this.indexOfStringForReadChar.set(integer+1); + this.indexOfStringForReadChar.set(readerCount+1); } - char c = readLine2[integer]; + char c = stringBuffer.getChar(readerCount); return c; } else { @@ -339,19 +356,30 @@ public class InputConsole { // it just waits until \n until it triggers if (this.unsupported) { BufferedReader reader = (BufferedReader) this.reader; + String line = null; + char[] readLine = null; try { - while ((this.readLine = reader.readLine().toCharArray()) != null) { + while ((line = reader.readLine()) != null) { + readLine = line.toCharArray(); + // notify everyone waiting for a line of text. synchronized (this.inputLockSingle) { - if (this.readLine.length > 0) { - this.readChar = this.readLine[0]; + if (readLine.length > 0) { + this.readChar = readLine[0]; } else { this.readChar = -1; } this.inputLockSingle.notifyAll(); } synchronized (this.inputLockLine) { + byte[] charToBytes = Sys.charToBytes(readLine); + + for (ByteBuffer2Fast buffer : this.threadBuffersForRead) { + buffer.clear(); + buffer.putBytes(charToBytes); + } + this.inputLockLine.notifyAll(); } } @@ -360,12 +388,13 @@ public class InputConsole { } } else { + // from a 'regular' console try { final boolean ansiEnabled = Ansi.isEnabled(); Ansi ansi = Ansi.ansi(); + PrintStream out = AnsiConsole.out; int typedChar; - StringBuilder buf = new StringBuilder(); // don't type ; in a bash shell, it quits everything // \n is replaced by \r in unix terminal? @@ -388,65 +417,57 @@ public class InputConsole { // clear ourself + one extra. if (ansiEnabled) { - // size of the buffer BEFORE our backspace was typed - int length = buf.length(); - int amtToOverwrite = 2; // backspace is always 2 chars (^?) + for (ByteBuffer2Fast buffer : this.threadBuffersForRead) { + // size of the buffer BEFORE our backspace was typed + int length = buffer.position(); + int amtToOverwrite = 2 * 2; // backspace is always 2 chars (^?) * 2 because it's bytes - if (length > 1) { - char charAt = buf.charAt(length-1); - amtToOverwrite += getPrintableCharacters(charAt); + if (length > 1) { + char charAt = buffer.getChar(length-2); + amtToOverwrite += getPrintableCharacters(charAt); - // delete last item in our buffer - buf.setLength(--length); + // delete last item in our buffer + length -= 2; + buffer.position(length); - // now figure out where the cursor is really at. - // this is more memory friendly than buf.toString.length - for (int i=0;i 0) { - this.readLine = new char[length]; - buf.getChars(0, length, this.readLine, 0); - } else { - this.readLine = emptyLine; - } - this.inputLockLine.notifyAll(); } - - // dump the characters in the backing array (slightly safer for passwords when using this method) - if (length > 0) { - buf.delete(0, buf.length()); - } } else if (asChar != '\r') { // only append if we are not a new line. - buf.append(asChar); + for (ByteBuffer2Fast buffer : this.threadBuffersForRead) { + buffer.putChar(asChar); + } } } } catch (IOException ignored) { @@ -455,11 +476,11 @@ public class InputConsole { } /** - + * Get the default encoding. Will first look at the LC_CTYPE environment variable, then the input.encoding - + * system property, then the default charset according to the JVM. - + * - + * @return The default encoding to use when none is specified. - + */ + * Get the default encoding. Will first look at the LC_CTYPE environment variable, then the input.encoding + * system property, then the default charset according to the JVM. + * + * @return The default encoding to use when none is specified. + */ public static String getEncoding() { // LC_CTYPE is usually in the form en_US.UTF-8 String envEncoding = extractEncodingFromCtype(System.getenv("LC_CTYPE")); @@ -481,9 +502,8 @@ public class InputConsole { String encodingAndModifier = ctype.substring(ctype.indexOf('.') + 1); if (encodingAndModifier.indexOf('@') > 0) { return encodingAndModifier.substring(0, encodingAndModifier.indexOf('@')); - } else { - return encodingAndModifier; } + return encodingAndModifier; } return null; } diff --git a/Dorkbox-Util/src/dorkbox/util/input/windows/WindowsTerminal.java b/Dorkbox-Util/src/dorkbox/util/input/windows/WindowsTerminal.java index 25cc950..4fece85 100644 --- a/Dorkbox-Util/src/dorkbox/util/input/windows/WindowsTerminal.java +++ b/Dorkbox-Util/src/dorkbox/util/input/windows/WindowsTerminal.java @@ -54,7 +54,6 @@ public class WindowsTerminal extends Terminal public void init() throws IOException { this.originalMode = WindowsSupport.getConsoleMode(); WindowsSupport.setConsoleMode(this.originalMode & ~ConsoleMode.ENABLE_ECHO_INPUT.code); - setEchoEnabled(false); } /**